5 mejores prácticas para construir APIs RESTful robustas en PHP
Este artículo va directo al grano: prácticas comprobadas para diseñar, implementar y mantener APIs RESTful en PHP que sean seguras, mantenibles y fáciles de escalar. Incluye estructura de proyecto, ejemplos de código, middleware esencial y por qué cada práctica importa.
Estructura mínima recomendada
Mantén claro el flujo entre entrada, lógica y persistencia:
my-api/
├─ public/
│ └─ index.php # punto de entrada (servidor web apunta aquí)
├─ src/
│ ├─ Controller/
│ │ └─ UserController.php
│ ├─ Repository/
│ │ └─ UserRepository.php
│ ├─ Middleware/
│ │ └─ AuthMiddleware.php
│ └─ bootstrap.php
├─ config/
│ └─ settings.php
├─ tests/
└─ composer.json
1) Entradas y respuestas: JSON, validación y errores consistentes
Forzar JSON, validar antes de tocar la lógica y devolver errores deterministas con códigos HTTP y un esquema fijo.
<?php
// public/index.php
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../src/bootstrap.php';
// Forzar JSON
if (strpos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') === false &&
in_array($_SERVER['REQUEST_METHOD'], ['POST','PUT','PATCH'])) {
http_response_code(415);
echo json_encode(['error' => 'Unsupported Media Type, use application/json']);
exit;
}
// Simple router (por claridad)
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
if ($path === '/users' && $method === 'POST') {
$body = json_decode(file_get_contents('php://input'), true);
if (!is_array($body)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
// Validación mínima
if (empty($body['email']) || !filter_var($body['email'], FILTER_VALIDATE_EMAIL)) {
http_response_code(422);
echo json_encode(['error' => 'Email inválido']);
exit;
}
// Lógica: delegar a repository
$repo = new UserRepository($pdo);
$id = $repo->create($body['email'], $body['name'] ?? null);
http_response_code(201);
echo json_encode(['id' => $id]);
exit;
}
http_response_code(404);
echo json_encode(['error' => 'Not found']);
Por qué: validar tempranamente evita efectos secundarios y asegura respuestas predecibles. Usa códigos HTTP correctos: 400, 401, 403, 404, 422, 500.
2) Acceso a datos: usa PDO con sentencias preparadas y un repository
<?php
// src/Repository/UserRepository.php
class UserRepository
{
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function create(string $email, ?string $name): int
{
$stmt = $this->pdo->prepare('INSERT INTO users (email, name) VALUES (:email, :name)');
$stmt->execute(['email' => $email, 'name' => $name]);
return (int) $this->pdo->lastInsertId();
}
public function findById(int $id): ?array
{
$stmt = $this->pdo->prepare('SELECT id, email, name FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
}
Por qué: PDO con prepared statements previene inyección SQL. Separar en repositories facilita testing y swapping de persistencia.
3) Autenticación y autorización: JWT + middleware
Implementa autenticación como middleware que valida el token antes de permitir acceso a controladores protegidos.
<?php
// src/Middleware/AuthMiddleware.php
use Firebase\JWT\JWT;
class AuthMiddleware
{
private $secret;
public function __construct(string $secret) { $this->secret = $secret; }
public function handle()
{
$h = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/^Bearer\s+(\S+)$/', $h, $m)) {
http_response_code(401);
echo json_encode(['error' => 'Missing token']);
exit;
}
$token = $m[1];
try {
$payload = JWT::decode($token, $this->secret, ['HS256']);
// Attach user to request context (simple example)
$GLOBALS['user'] = $payload;
} catch (Exception $e) {
http_response_code(401);
echo json_encode(['error' => 'Invalid token']);
exit;
}
}
}
Por qué: JWT es simple y sin estado para APIs. Ten cuidado: almacena solo IDs y expiración, y rota secretos periódicamente.
4) Rate limiting, caching y paginación
Protege tu API de abusos y ofrece respuestas eficientes:
- Rate limiting: usar Redis para contar peticiones por IP/usuario.
- Caching: respuestas idempotentes (GET) con ETag o Cache-Control.
- Paginación: usa limit/offset o keyset pagination; incluye enlaces next/prev.
<?php
// Simple rate limit pseudo-código usando Redis
$ip = $_SERVER['REMOTE_ADDR'];
$key = "rate:{$ip}";
$remaining = $redis->incr($key);
if ($remaining == 1) $redis->expire($key, 60); // window 60s
$limit = 60;
if ($remaining > $limit) {
http_response_code(429);
header('Retry-After: 60');
echo json_encode(['error' => 'Too many requests']);
exit;
}
5) Documentación, testing y observabilidad
Documenta con OpenAPI (swagger) y cubre lo crítico con tests automatizados; agrega logs estructurados y métricas (latencia, error rate).
// Ejemplo mínimo de test con PHPUnit (tests/UserTest.php)
public function testCreateUser()
{
$response = $this->httpPost('/users', ['email' => 'a@b.com', 'name' => 'A']);
$this->assertEquals(201, $response->getStatusCode());
$body = json_decode($response->getBody(), true);
$this->assertArrayHasKey('id', $body);
}
Por qué: documentación y tests reducen fricción para integradores y evitan regresiones.
Buenas prácticas rápidas (lista)
- Versiona tu API (/v1/users) desde el principio.
- No reveles detalles internos en errores de 500.
- Usa HTTPS obligatorio y HSTS.
- Límita campos serializados (avoid overfetching) y permite includes.
- Gestiona CORS de forma explícita, no con comodines en producción.
Por qué enfocarse en estas prácticas
Las APIs fallan por problemas repetibles: validación insuficiente, falta de límites, secretos mal gestionados y malas practicas de persistencia. Adoptar patrones simples —middleware para cross-cutting concerns, repositorios para datos, respuestas consistentes— reduce errores y acelera el desarrollo.
Siguiente paso avanzado
Aplica un enfoque schema-first: define tu API en OpenAPI, genera contratos y pruebas automáticas; añade un gateway (por ejemplo Kong o Amazon API Gateway) para centralizar rate limiting, logging y autenticación. Atención: audita secretos y rotaciones si usas JWT en producción.
Advertencia: incluso con JWT, protege endpoints críticos con scopes y checks en el backend; nunca confíes solo en expiraciones largas.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación