/productos) que se manipulan con métodos HTTP (GET, POST,
PUT,
DELETE) y responden en JSON con el código de estado adecuado. En PHP basta un archivo que lea el método,
consulte MySQL con PDO y devuelva json_encode(); en el navegador, fetch hace
el
resto.
Cuando tu frontend necesita datos sin recargar la página, o cuando quieres que una app móvil use el mismo backend que tu web, necesitas una API. Es el punto en el que tu proyecto deja de ser “páginas PHP” y pasa a ser una arquitectura full stack de verdad. El libro Programación Full-Stack dedica un capítulo a consumir y crear APIs RESTful; aquí construimos una sobre el CRUD de productos.
Qué es una API REST, en concreto
REST es un estilo de diseño con unas pocas ideas clave:
- Recursos con URL propia:
/api/productoses la colección;/api/productos/5, un elemento. - El método HTTP indica la acción, en lugar de URLs como
/borrarProducto. - Respuestas en JSON con códigos de estado que significan algo.
- Sin estado: cada petición trae todo lo necesario para procesarla (por ejemplo, el token de autenticación).
Diseña los endpoints
| Método | Ruta | Acción | Respuesta correcta |
|---|---|---|---|
GET |
/api/productos |
Listar | 200 + array |
GET |
/api/productos/5 |
Ver uno | 200, o 404 si no existe |
POST |
/api/productos |
Crear | 201 + el producto creado |
PUT |
/api/productos/5 |
Reemplazar | 200 + el producto |
DELETE |
/api/productos/5 |
Eliminar | 204 sin cuerpo |
Y para los errores: 400 o 422 si los datos no son válidos, 401 si
falta autenticación, 404 si el recurso no existe, 405 si el método no está
permitido y 500 si falla el servidor.
La API en PHP
Para tener URLs limpias, un .htaccess redirige todas las rutas de /api/ a un
solo
archivo:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
<?php
declare(strict_types=1);
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../helpers.php'; // validarProducto()
header('Content-Type: application/json; charset=utf-8');
function responder(int $codigo, $datos = null): void
{
http_response_code($codigo);
if ($datos !== null) {
echo json_encode($datos, JSON_UNESCAPED_UNICODE);
}
exit;
}
// "/mi-app/api/productos/5" → ['productos', '5']
$ruta = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$tramo = substr($ruta, strpos($ruta, '/api/') + strlen('/api/'));
$partes = explode('/', trim($tramo, '/'));
$recurso = $partes[0] ?? '';
$id = isset($partes[1]) ? filter_var($partes[1], FILTER_VALIDATE_INT) : null;
$metodo = $_SERVER['REQUEST_METHOD'];
if ($recurso !== 'productos') {
responder(404, ['error' => 'Recurso no encontrado']);
}
try {
// GET /productos
if ($metodo === 'GET' && $id === null) {
$filas = db()->query('SELECT id, nombre, precio, stock FROM productos ORDER BY id DESC')->fetchAll();
responder(200, $filas);
}
// GET /productos/5
if ($metodo === 'GET') {
$stmt = db()->prepare('SELECT id, nombre, precio, stock FROM productos WHERE id = ?');
$stmt->execute([$id]);
$producto = $stmt->fetch();
$producto ? responder(200, $producto) : responder(404, ['error' => 'Producto no encontrado']);
}
// POST /productos — el cuerpo llega como JSON, no en $_POST
if ($metodo === 'POST' && $id === null) {
$entrada = json_decode(file_get_contents('php://input'), true) ?? [];
[$datos, $errores] = validarProducto($entrada);
if ($errores) {
responder(422, ['errores' => $errores]);
}
$stmt = db()->prepare('INSERT INTO productos (nombre, precio, stock) VALUES (:nombre, :precio, :stock)');
$stmt->execute($datos);
responder(201, ['id' => (int) db()->lastInsertId()] + $datos);
}
// DELETE /productos/5
if ($metodo === 'DELETE' && $id) {
$stmt = db()->prepare('DELETE FROM productos WHERE id = ?');
$stmt->execute([$id]);
$stmt->rowCount() ? responder(204) : responder(404, ['error' => 'Producto no encontrado']);
}
responder(405, ['error' => 'Método no permitido']);
} catch (Throwable $e) {
error_log($e->getMessage());
responder(500, ['error' => 'Error interno']);
}
Detalles que marcan la diferencia:
- Cuando el cliente envía JSON, los datos no llegan a
$_POST: se leen dephp://inputy se decodifican. JSON_UNESCAPED_UNICODEmantiene las tildes legibles en la respuesta.- Reutilizamos
validarProducto()del CRUD: la API y el panel aplican las mismas reglas. - El
catchregistra el error real en el log y devuelve un mensaje genérico.
UPDATE ... WHERE id =
:id y respondiendo 404 si el producto no existe.
Pruébala con curl
$ curl http://localhost/mi-app/api/productos
$ curl -X POST http://localhost/mi-app/api/productos \
-H "Content-Type: application/json" \
-d '{"nombre":"Webcam HD","precio":32000,"stock":8}'
$ curl -i -X DELETE http://localhost/mi-app/api/productos/3
La opción -i muestra las cabeceras, útil para comprobar que el código de estado es el que
esperas. Si prefieres una interfaz gráfica, Postman o la extensión Thunder Client de VS Code hacen lo
mismo.
Consúmela desde JavaScript
// Cárgalo con <script type="module" src="app.js"> para poder usar await fuera de funciones
const API = 'api/productos';
async function api(url, opciones = {}) {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
...opciones,
});
if (res.status === 204) return null;
const data = await res.json();
if (!res.ok) throw data; // { error } o { errores }
return data;
}
// Listar
const productos = await api(API);
// Crear
try {
const nuevo = await api(API, {
method: 'POST',
body: JSON.stringify({ nombre: 'Webcam HD', precio: 32000, stock: 8 }),
});
console.log('Creado con id', nuevo.id);
} catch (e) {
console.warn(e.errores ?? e.error);
}
// Eliminar
await api(`${API}/3`, { method: 'DELETE' });
Si quieres repasar fetch, async/await y cómo pintar los resultados en el DOM,
tienes todo en el artículo sobre JavaScript y el DOM.
Errores comunes
- Responder 200 a todo y poner el error dentro del JSON: el cliente no puede distinguir un éxito de un fallo sin leer el cuerpo.
- Avisos de PHP mezclados con el JSON: un
Warningimpreso antes de la respuesta rompe elJSON.parse. En la API, registra los errores en el log en lugar de mostrarlos. - Errores de CORS: si el frontend está en otro dominio, la API debe enviar
Access-Control-Allow-Origincon ese dominio concreto, no con*si maneja datos privados. - Endpoints sin autenticación: todo lo que crea, modifica o borra debe exigir un usuario autenticado, ya sea con la sesión o con un token.
Completa el stack: de la API al despliegue
El libro Programación Full-Stack sigue con el despliegue en producción y tres proyectos reales donde frontend, API y base de datos trabajan juntos.
Comprar en Amazon