En esta guía vamos a construir una API REST con PHP desde cero utilizando JSON, los métodos HTTP GET, POST, PUT y DELETE, códigos de estado y PDO para trabajar con MySQL.
La idea es entender cómo funciona una API por dentro antes de pasar a frameworks como Laravel. Al finalizar vas a tener una API capaz de listar, consultar, crear, modificar y eliminar registros desde clientes como Postman, JavaScript, aplicaciones móviles u otros sistemas.
¿Qué vamos a construir?
- una API que devuelva respuestas JSON;
- endpoint GET para listar usuarios;
- endpoint GET para consultar un usuario por ID;
- endpoint POST para crear usuarios;
- endpoint PUT para modificarlos;
- endpoint DELETE para eliminarlos;
- validación de datos;
- consultas preparadas con PDO;
- códigos HTTP correctos;
- manejo básico de errores;
- pruebas desde Postman.
Antes de empezar
Conviene haber visto previamente estas guías:
- PHP y MySQL con PDO
- CRUD completo con PHP, MySQL y PDO
- Proyecto final PHP: login, roles, CRUD, PDO y MVC
1. ¿Qué es una API REST?
Una API permite que dos aplicaciones intercambien información. En una API REST los recursos se identifican mediante URLs y las acciones se expresan principalmente a través de métodos HTTP.
| Método | Uso habitual | Ejemplo |
|---|---|---|
| GET | Consultar datos | /api/usuarios |
| POST | Crear un recurso | /api/usuarios |
| PUT | Actualizar un recurso | /api/usuarios/5 |
| DELETE | Eliminar un recurso | /api/usuarios/5 |
Una API no debería depender de páginas HTML. En este caso recibirá y devolverá información en formato JSON.
2. Estructura del proyecto
api-php/
├── config/
│ └── database.php
├── src/
│ ├── UsuarioRepository.php
│ └── response.php
└── public/
└── index.php
Para mantener el ejemplo claro vamos a utilizar un único punto de entrada público y separar la conexión y el acceso a datos.
3. Crear la base de datos
CREATE DATABASE api_php CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE api_php; CREATE TABLE usuarios ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, nombre VARCHAR(100) NOT NULL, email VARCHAR(150) NOT NULL UNIQUE, activo TINYINT(1) NOT NULL DEFAULT 1, creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
4. Conexión con PDO
Archivo config/database.php:
<?php return new PDO( 'mysql:host=localhost;dbname=api_php;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ] );
5. Responder siempre en JSON
Podemos crear una función común para todas las respuestas.
<?php function responder( array $datos, int $status = 200 ): never { http_response_code($status); header('Content-Type: application/json; charset=utf-8'); echo json_encode( $datos, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); exit; }
Así evitamos repetir encabezados, código de estado y json_encode() en cada endpoint.
6. Leer el método HTTP y la URL
$metodo = $_SERVER['REQUEST_METHOD']; $ruta = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); $segmentos = array_values( array_filter(explode('/', $ruta)) );
Para una URL como /api/usuarios/5, los segmentos serán aproximadamente:
[
'api',
'usuarios',
'5'
]
7. Repositorio de usuarios
<?php class UsuarioRepository { public function __construct( private PDO $pdo ) { } public function todos(): array { $stmt = $this->pdo->query( 'SELECT id, nombre, email, activo, creado_en FROM usuarios ORDER BY id DESC' ); return $stmt->fetchAll(); } public function buscar(int $id): ?array { $stmt = $this->pdo->prepare( 'SELECT id, nombre, email, activo, creado_en FROM usuarios WHERE id = :id' ); $stmt->execute(['id' => $id]); $usuario = $stmt->fetch(); return $usuario ?: null; } }
8. GET: listar usuarios
Para GET /api/usuarios:
if ( $metodo === 'GET' && count($segmentos) === 2 ) { $usuarios = $repository->todos(); responder([ 'success' => true, 'data' => $usuarios ]); }
Una respuesta posible sería:
{
"success": true,
"data": [
{
"id": 1,
"nombre": "Ana",
"email": "ana@ejemplo.com"
}
]
}
9. GET: consultar un usuario por ID
if ( $metodo === 'GET' && count($segmentos) === 3 ) { $id = filter_var( $segmentos[2], FILTER_VALIDATE_INT ); if (!$id) { responder([ 'success' => false, 'message' => 'ID inválido' ], 400); } $usuario = $repository->buscar($id); if (!$usuario) { responder([ 'success' => false, 'message' => 'Usuario no encontrado' ], 404); } responder([ 'success' => true, 'data' => $usuario ]); }
10. POST: crear un usuario
Cuando enviamos JSON, PHP no lo coloca automáticamente en $_POST. Debemos leer el cuerpo crudo de la petición.
$datos = json_decode(
file_get_contents('php://input'),
true
);
if (!is_array($datos)) {
responder([
'success' => false,
'message' => 'JSON inválido'
], 400);
}
Después validamos los campos:
$nombre = trim($datos['nombre'] ?? ''); $email = trim($datos['email'] ?? ''); if ($nombre === '') { responder([ 'success' => false, 'message' => 'El nombre es obligatorio' ], 422); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { responder([ 'success' => false, 'message' => 'El email no es válido' ], 422); }
La inserción se realiza mediante una consulta preparada:
$stmt = $pdo->prepare(
'INSERT INTO usuarios (nombre, email)
VALUES (:nombre, :email)'
);
$stmt->execute([
'nombre' => $nombre,
'email' => $email
]);
$id = (int) $pdo->lastInsertId();
responder([
'success' => true,
'message' => 'Usuario creado',
'id' => $id
], 201);
11. PUT: actualizar un usuario
$stmt = $pdo->prepare(
'UPDATE usuarios
SET nombre = :nombre,
email = :email,
activo = :activo
WHERE id = :id'
);
$stmt->execute([
'nombre' => $nombre,
'email' => $email,
'activo' => $activo,
'id' => $id
]);
responder([
'success' => true,
'message' => 'Usuario actualizado'
]);
Antes de ejecutar el UPDATE conviene verificar que el recurso exista. De lo contrario una petición podría responder “actualizado” aunque el ID nunca haya existido.
12. DELETE: eliminar un usuario
$stmt = $pdo->prepare(
'DELETE FROM usuarios WHERE id = :id'
);
$stmt->execute([
'id' => $id
]);
responder([
'success' => true,
'message' => 'Usuario eliminado'
]);
En sistemas reales puede convenir una baja lógica utilizando un campo como activo o deleted_at, especialmente cuando se necesita conservar historial.
13. Códigos HTTP que conviene usar
| Código | Significado |
|---|---|
| 200 | Petición correcta |
| 201 | Recurso creado |
| 204 | Respuesta correcta sin contenido |
| 400 | Petición inválida |
| 401 | No autenticado |
| 403 | No autorizado |
| 404 | Recurso no encontrado |
| 409 | Conflicto, por ejemplo email duplicado |
| 422 | Datos válidos como JSON pero incorrectos para la operación |
| 500 | Error interno del servidor |
14. Manejar errores de base de datos
try { // ejecutar operación } catch (PDOException $e) { responder([ 'success' => false, 'message' => 'Error interno del servidor' ], 500); }
En producción no conviene devolver al cliente el mensaje completo de la excepción porque podría revelar nombres de tablas, columnas o detalles de infraestructura. Ese detalle debería registrarse en logs.
15. Router completo simplificado
if ($segmentos[0] ?? null !== 'api') { responder([ 'success' => false, 'message' => 'Ruta no encontrada' ], 404); } if (($segmentos[1] ?? null) !== 'usuarios') { responder([ 'success' => false, 'message' => 'Recurso no encontrado' ], 404); } $id = isset($segmentos[2]) ? (int) $segmentos[2] : null; switch ($metodo) { case 'GET': // listar o buscar por ID break; case 'POST': // crear break; case 'PUT': // actualizar break; case 'DELETE': // eliminar break; default: responder([ 'success' => false, 'message' => 'Método no permitido' ], 405); }
16. Probar la API con Postman
Supongamos que el proyecto corre en http://localhost/api-php/public.
- GET
/api/usuarios→ listar. - GET
/api/usuarios/1→ consultar un usuario. - POST
/api/usuarios→ crear. - PUT
/api/usuarios/1→ actualizar. - DELETE
/api/usuarios/1→ eliminar.
Para POST y PUT seleccioná Body → raw → JSON y enviá algo como:
{
"nombre": "Juan Pérez",
"email": "juan@ejemplo.com"
}
17. ¿Qué pasa con CORS?
Si un frontend JavaScript se ejecuta desde otro origen, el navegador puede bloquear la petición por la política de mismo origen. Para desarrollo podríamos permitir un origen concreto:
header(
'Access-Control-Allow-Origin: https://mi-frontend.com'
);
header(
'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'
);
header(
'Access-Control-Allow-Headers: Content-Type, Authorization'
);
No conviene usar * indiscriminadamente en producción cuando la API maneja credenciales o información sensible.
18. Buenas prácticas básicas
- usar HTTPS en producción;
- validar todos los datos recibidos;
- usar consultas preparadas;
- no devolver excepciones completas al cliente;
- usar códigos HTTP coherentes;
- mantener una estructura JSON consistente;
- separar acceso a datos de la lógica HTTP;
- registrar errores en logs;
- versionar la API cuando empiece a crecer, por ejemplo
/api/v1/usuarios; - agregar autenticación antes de exponer operaciones sensibles.
19. ¿Cómo quedaría una respuesta consistente?
Éxito:
{
"success": true,
"data": {
"id": 1,
"nombre": "Juan Pérez"
}
}
Error:
{
"success": false,
"message": "Usuario no encontrado"
}
20. Siguiente paso: autenticación
Esta API todavía es pública. Cualquier cliente que conozca la URL podría intentar crear, modificar o eliminar datos. El siguiente paso natural es implementar autenticación por token y proteger determinados endpoints.
Eso nos permitirá trabajar conceptos como:
- login mediante API;
- generación de tokens;
- header
Authorization; - endpoints públicos y privados;
- expiración o revocación de tokens;
- permisos por usuario o rol.
¿Qué aprendimos?
Construimos la base de una API REST con PHP puro y MySQL. Vimos cómo interpretar métodos HTTP, leer JSON, devolver respuestas JSON, utilizar códigos de estado, validar datos y ejecutar operaciones CRUD mediante PDO.
Entender este flujo antes de utilizar un framework permite comprender mejor qué hacen Laravel, Symfony u otras herramientas cuando construimos APIs más grandes.
Seguí aprendiendo PHP
Podés recorrer toda la ruta desde:
Aprender PHP desde cero: ruta completa →
¿Te sirvió esta guía? ☕
Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando guías, ejercicios y proyectos gratuitos, podés colaborar mediante:
Descubre más desde Club Programador
Suscríbete y recibe las últimas entradas en tu correo electrónico.
Un comentario en “API REST con PHP desde cero: GET, POST, PUT, DELETE, JSON y PDO”