Cómo construir y consumir una API REST de Usuarios en PHP
Una API REST sencilla pero completa: sin frameworks, con caché, CRUD completo y respuestas JSON estandarizadas. En este post te explico cómo la construí y cómo consumirla desde cualquier cliente.
¿Qué hace esta API?
- CRUD completo sobre usuarios (GET, POST, PUT, DELETE)
- Caché en archivo JSON con TTL de 5 minutos
- Filtros en GET: por ID, por rol, búsqueda por nombre, límite de resultados y selección de campos
- CORS habilitado para cualquier origen
- Respuestas estandarizadas con
status,code,messageydata
Estructura de archivos
/
├── api.php # Router principal y manejadores HTTP
├── helpers.php # Funciones reutilizables (caché, validaciones, respuestas)
├── usuarios.json # "Base de datos" en JSON
└── cache.json # Almacén de caché
El código
helpers.php — El motor de la API
Contiene todas las funciones auxiliares organizadas en bloques:
Constantes y rutas de archivos:
define('USUARIOS_FILE', __DIR__ . '/usuarios.json');
define('CACHE_FILE', __DIR__ . '/cache.json');
define('CACHE_TTL', 300); // 5 minutos
Lectura y escritura de usuarios:
function leerUsuarios() {
if (!file_exists(USUARIOS_FILE)) {
return ['usuarios' => []];
}
$json = file_get_contents(USUARIOS_FILE);
return json_decode($json, true) ?? ['usuarios' => []];
}
function guardarUsuarios($data) {
$json = json_encode(
['usuarios' => $data],
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);
file_put_contents(USUARIOS_FILE, $json);
}
Sistema de caché:
// Obtener dato del caché (devuelve null si expiró o no existe)
function obtenerDelCache($clave) {
$cache = limpiarCacheExpirado();
if (!isset($cache[$clave])) return null;
$item = $cache[$clave];
$ahora = time();
if (isset($item['timestamp']) && ($ahora - $item['timestamp'] > CACHE_TTL)) {
return null;
}
return $item['data'] ?? null;
}
// Guardar un dato en caché con timestamp actual
function guardarEnCache($clave, $datos) {
$cache = leerCache();
$cache[$clave] = [
'data' => $datos,
'timestamp' => time()
];
guardarCache($cache);
}
// Invalida todo el caché (o solo una clave)
function invalidarCache($clave = null) {
if ($clave === null) {
guardarCache([]);
return;
}
$cache = leerCache();
unset($cache[$clave]);
guardarCache($cache);
}
El caché se invalida automáticamente en cada operación de escritura (POST, PUT, DELETE), y se limpia de entradas expiradas antes de cada lectura.
Validaciones:
function validarCamposObligatorios($datos, $campos = ['nombre', 'email', 'rol']) {
foreach ($campos as $campo) {
if (!isset($datos[$campo]) || trim($datos[$campo]) === '') {
return ['valido' => false, 'mensaje' => "El campo '$campo' es obligatorio"];
}
}
return ['valido' => true];
}
function validarEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
Respuesta estandarizada:
function responder($status, $code, $mensaje = '', $datos = null) {
http_response_code($code);
$respuesta = ['status' => $status, 'code' => $code];
if ($mensaje) $respuesta['message'] = $mensaje;
if ($datos !== null) {
if (is_array($datos) && isset($datos['total'])) {
$respuesta['total'] = $datos['total'];
$respuesta['data'] = $datos['data'];
} else {
$respuesta['data'] = $datos;
}
}
echo json_encode($respuesta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
api.php — El router
Maneja los headers CORS, detecta el método HTTP y delega a la función correspondiente:
header('Content-Type: application/json; charset=UTF-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
require_once __DIR__ . '/helpers.php';
$metodo = $_SERVER['REQUEST_METHOD'];
$id = $_GET['id'] ?? null;
switch ($metodo) {
case 'GET': manejarGET($id); break;
case 'POST': manejarPOST(); break;
case 'PUT': manejarPUT($id); break;
case 'DELETE': manejarDELETE($id); break;
default: responder('error', 405, 'Método HTTP no permitido');
}
Manejador GET con caché:
function manejarGET($id) {
$rol = $_GET['rol'] ?? null;
$buscar = $_GET['buscar'] ?? null;
$limite = $_GET['limite'] ?? null;
$campos = $_GET['campos'] ?? null;
// Clave única basada en todos los parámetros
$claveCaché = 'usuarios_' . md5(json_encode(compact('id','rol','buscar','limite','campos')));
$datosEnCaché = obtenerDelCache($claveCaché);
if ($datosEnCaché !== null) {
responder('success', 200, '', $datosEnCaché);
}
$usuarios = leerUsuarios()['usuarios'];
if ($id) {
$usuario = buscarUsuarioPorId($usuarios, $id);
if (!$usuario) responder('error', 404, 'Usuario no encontrado');
if ($campos) $usuario = seleccionarCampos([$usuario], $campos)[0];
guardarEnCache($claveCaché, $usuario);
responder('success', 200, '', $usuario);
}
if ($rol) $usuarios = filtrarPorRol($usuarios, $rol);
if ($buscar) $usuarios = buscarPorNombre($usuarios, $buscar);
if ($limite) $usuarios = limitarResultados($usuarios, $limite);
if ($campos) $usuarios = seleccionarCampos($usuarios, $campos);
$usuarios = array_values($usuarios);
$respuesta = ['total' => count($usuarios), 'data' => $usuarios];
guardarEnCache($claveCaché, $respuesta);
responder('success', 200, '', $respuesta);
}
Manejador POST:
function manejarPOST() {
$rawInput = file_get_contents('php://input');
if (empty($rawInput)) responder('error', 400, 'Body vacío o inválido');
$input = json_decode($rawInput, true);
if ($input === null) responder('error', 400, 'JSON inválido');
$validacion = validarCamposObligatorios($input);
if (!$validacion['valido']) responder('error', 400, $validacion['mensaje']);
$usuarios = leerUsuarios()['usuarios'];
$nuevoId = obtenerProximoId($usuarios);
$nuevoUsuario = [
'id' => $nuevoId,
'nombre' => $input['nombre'],
'email' => $input['email'],
'rol' => $input['rol']
];
// Preservar campos extra opcionales
foreach ($input as $clave => $valor) {
if (!in_array($clave, ['nombre', 'email', 'rol'])) {
$nuevoUsuario[$clave] = $valor;
}
}
$usuarios[] = $nuevoUsuario;
guardarUsuarios($usuarios);
invalidarCache();
responder('success', 201, 'Usuario creado exitosamente', $nuevoUsuario);
}
Manejador PUT:
function manejarPUT($id) {
if (!$id) responder('error', 400, 'El parámetro "id" es requerido');
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input)) responder('error', 400, 'No hay campos para actualizar');
$usuarios = leerUsuarios()['usuarios'];
$indice = encontrarIndiceUsuario($usuarios, $id);
if ($indice === -1) responder('error', 404, 'Usuario no encontrado');
// Merge: los campos no enviados mantienen su valor original
$usuarioActual = $usuarios[$indice];
foreach ($input as $clave => $valor) {
$usuarioActual[$clave] = $valor;
}
$usuarios[$indice] = $usuarioActual;
guardarUsuarios($usuarios);
invalidarCache();
responder('success', 200, 'Usuario actualizado exitosamente', $usuarioActual);
}
Manejador DELETE:
function manejarDELETE($id) {
if (!$id) responder('error', 400, 'El parámetro "id" es requerido');
$usuarios = leerUsuarios()['usuarios'];
$indice = encontrarIndiceUsuario($usuarios, $id);
if ($indice === -1) responder('error', 404, 'Usuario no encontrado');
$usuarioEliminado = $usuarios[$indice];
unset($usuarios[$indice]);
guardarUsuarios(array_values($usuarios));
invalidarCache();
responder('success', 200, 'Usuario eliminado exitosamente', $usuarioEliminado);
}
Estructura de datos
usuarios.json — almacena todos los usuarios:
{
"usuarios": [
{ "id": 1, "nombre": "Ana Updated", "email": "ana@ejemplo.com", "rol": "admin" },
{ "id": 3, "nombre": "Marta López", "email": "marta@ejemplo.com", "rol": "viewer" },
{ "id": 4, "nombre": "test2", "email": "test2@test2.com", "rol": "admin" }
]
}
cache.json — guarda respuestas cacheadas con su timestamp:
{
"usuarios_21b9fbe5059d18217996ad562afffdc5": {
"data": {
"total": 3,
"data": [
{
"id": 1,
"nombre": "Ana Updated",
"email": "ana@ejemplo.com",
"rol": "admin"
},
{
"id": 3,
"nombre": "Marta López",
"email": "marta@ejemplo.com",
"rol": "viewer"
},
{
"id": 4,
"nombre": "test2",
"email": "test2@test2.com",
"rol": "admin"
}
]
},
"timestamp": 1772726700
}
}
Cómo consumir la API
GET — Listar todos los usuarios
curl -X GET "http://tu-servidor/api.php"
{
"status": "success",
"code": 200,
"total": 3,
"data": [
{ "id": 1, "nombre": "Ana Updated", "email": "ana@ejemplo.com", "rol": "admin" },
...
]
}
GET — Filtros disponibles
# Por ID
curl "http://tu-servidor/api.php?id=1"
# Por rol
curl "http://tu-servidor/api.php?rol=admin"
# Búsqueda por nombre (parcial, case-insensitive)
curl "http://tu-servidor/api.php?buscar=ana"
# Limitar resultados
curl "http://tu-servidor/api.php?limite=2"
# Seleccionar solo ciertos campos
curl "http://tu-servidor/api.php?campos=id,nombre,email"
# Combinados
curl "http://tu-servidor/api.php?rol=admin&campos=id,nombre&limite=5"
POST — Crear usuario
curl -X POST "http://tu-servidor/api.php" \
-H "Content-Type: application/json" \
-d '{"nombre": "Carlos Ruiz", "email": "carlos@ejemplo.com", "rol": "editor"}'
{
"status": "success",
"code": 201,
"message": "Usuario creado exitosamente",
"data": {
"id": 5,
"nombre": "Carlos Ruiz",
"email": "carlos@ejemplo.com",
"rol": "editor"
}
}
PUT — Actualizar usuario
curl -X PUT "http://tu-servidor/api.php?id=5" \
-H "Content-Type: application/json" \
-d '{"nombre": "Carlos R. Actualizado"}'
Solo se actualizan los campos enviados. El resto se preserva tal como estaba.
DELETE — Eliminar usuario
curl -X DELETE "http://tu-servidor/api.php?id=5"
{
"status": "success",
"code": 200,
"message": "Usuario eliminado exitosamente",
"data": {
"id": 5,
"nombre": "Carlos R. Actualizado",
"email": "carlos@ejemplo.com",
"rol": "editor"
}
}
Consumo desde JavaScript (Fetch API)
const BASE_URL = 'http://tu-servidor/api.php';
// Listar usuarios
const res = await fetch(BASE_URL);
const json = await res.json();
console.log(json.data);
// Crear usuario
await fetch(BASE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nombre: 'Luis', email: 'luis@ejemplo.com', rol: 'viewer' })
});
// Actualizar usuario
await fetch(`${BASE_URL}?id=1`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rol: 'admin' })
});
// Eliminar usuario
await fetch(`${BASE_URL}?id=1`, { method: 'DELETE' });
Tabla de endpoints
| Método | URL | Descripción |
|---|---|---|
GET | /api.php | Lista todos los usuarios |
GET | /api.php?id=1 | Obtiene un usuario por ID |
GET | /api.php?rol=admin | Filtra por rol |
GET | /api.php?buscar=ana | Busca por nombre |
GET | /api.php?limite=5 | Limita resultados |
GET | /api.php?campos=id,nombre | Selecciona campos |
POST | /api.php | Crea un usuario nuevo |
PUT | /api.php?id=1 | Actualiza un usuario |
DELETE | /api.php?id=1 | Elimina un usuario |
Códigos de respuesta
| Código | Significado |
|---|---|
200 | OK — operación exitosa |
201 | Created — usuario creado |
400 | Bad Request — faltan campos o JSON inválido |
404 | Not Found — usuario no encontrado |
405 | Method Not Allowed — método HTTP no soportado |
Posibles mejoras
- Autenticación con JWT o API keys en el header
Authorization - Validación de email llamando a
validarEmail()antes de crear/actualizar - Base de datos real (MySQL/PostgreSQL) en lugar de JSON plano
- Paginación con parámetros
paginaypor_pagina - Caché en memoria con Redis para mayor rendimiento
- Logs de errores y auditoría de cambios
Una API funcional, sin dependencias, que puedes tener corriendo en cualquier servidor PHP en minutos.