[Guía de Campo Práctica] Construir una Plataforma de API Robusta con Laravel
REST / JSON:API / GraphQL, OpenAPI, versionado, ETag / peticiones condicionales, limitación de tasa, idempotencia, diseño de errores y documentación accesible
Lo que aprenderás en este artículo
- Cómo elegir entre REST, JSON:API y GraphQL, y pautas de implementación en Laravel
- Desarrollo guiado por especificación con OpenAPI (OAS) y validación de solicitudes/respuestas
- Estrategias de versionado (URI / cabecera / subdominio) y migración para cambios incompatibles
- Uso de ETag / If-None-Match e If-Modified-Since para caché y reducción de ancho de banda
- Técnicas prácticas de fiabilidad como limitación de tasa, idempotencia y webhooks firmados
- Representación de errores (problem+json de RFC 7807), mensajes multilingües y trace IDs
- Cómo hacer que la documentación de API, los ejemplos y los SDK sean accesibles y fáciles de entender para todo el mundo
- Pruebas (Feature / Contract), monitorización y comprobaciones de compatibilidad en operación
Lectores objetivo
- Ingenieros backend de Laravel de nivel principiante a intermedio: quieren exponer APIs con seguridad y evitar cambios incompatibles
- Tech leads / PMs: quieren ejecutar desarrollo guiado por especificación y gestión de releases centrados en OpenAPI
- Responsables de QA / documentación: quieren ofrecer especificaciones de API, tutoriales y ejemplos en una forma accesible y legible
- Desarrolladores que construyen integraciones: quieren patrones estándar para webhooks, verificación de firmas e idempotencia
Nivel de accesibilidad: ★★★★★
Definiremos de forma concreta una política no solo para la disponibilidad de la API en sí, sino también para hacer que la documentación, los tutoriales y el código de ejemplo sean comprensibles para cualquiera. Esto incluye representaciones independientes del color, compatibilidad con lectores de pantalla y contraste, navegación solo con teclado, copia sencilla de ejemplos, texto alternativo para diagramas y más.
1. Introducción: Una API es tanto un “contrato” como un “bien público”
Una API no es solo un conjunto de endpoints. Es un contrato que debe respetarse con los clientes a largo plazo. Los cambios incompatibles y las semánticas ambiguas se traducirán en caídas de servicio y carga para soporte.
Laravel tiene prácticamente todo lo que necesitas para construir APIs: autenticación, autorización, enrutamiento, validación, caché, limitación de tasa… pero sin principios de diseño claros, esas herramientas no brillan.
En este artículo construiremos paso a paso una plataforma de API robusta y legible, con el desarrollo guiado por especificación y la compatibilidad como ejes principales.
2. Elección de estilo: REST / JSON:API / GraphQL
2.1 Comparación rápida
-
REST (JSON genérico)
- Ventajas: Bajo coste de aprendizaje, se mantiene cercano a las semánticas HTTP. Fácil aprovechar CDN/caché.
- Desventajas: Muy flexible; la representación puede variar mucho entre implementaciones.
-
JSON:API (REST conforme a especificación)
- Ventajas: Convenciones claras para documentación, errores, recursos relacionados, paginación. Implementaciones cliente más sencillas.
- Desventajas: Como la especificación es estricta, hay una curva de aprendizaje inicial.
-
GraphQL
- Ventajas: Resuelve el over/under-fetching. Ideal para pantallas complejas.
- Desventajas: El caché HTTP es más difícil. Los problemas de N+1 y el mantenimiento del esquema pueden ser complicados.
Para APIs de negocio pequeñas o estándar, REST (o JSON:API) suele ser suficiente. Cuando tengas dificultades con agregaciones complejas para dashboards o con la optimización de pantallas móviles, GraphQL merece la pena.
Evita mezclar demasiados estilos: complica la operación. Empieza con REST como base y estandariza primero errores, autenticación y paginación.
3. Desarrollo guiado por especificación: OpenAPI en el centro
3.1 Tratar OAS como la única fuente de verdad
Utiliza la OpenAPI Specification (OAS) como tu única fuente de verdad, y deriva/valida servidor, clientes, pruebas y documentación a partir de ella.
Ventajas:
- Menos ambigüedad en los requisitos
- Detección más sencilla de cambios incompatibles
- Clientes más seguros mediante SDKs/tipos generados
3.2 Ejemplo de estructura de directorios
api/
├─ openapi.yaml # Especificación (mantenida por personas + validada en CI)
├─ examples/ # Ejemplos concretos de solicitudes/respuestas
└─ schemas/ # JSON Schemas reutilizables
app/
└─ Http/
├─ Controllers/Api/
├─ Middleware/
└─ Requests/Api/
tests/
└─ Contract/ # Pruebas de contrato contra OAS
3.3 Seguridad de tipos y validación en Laravel
- Escribe en cada
FormRequestlas mismas restricciones que en OAS - Además, utiliza
league/openapi-psr7-validatoro similar en las pruebas de contrato para verificar que las respuestas reales cumplen la especificación - Usa DTOs mediante
spatie/laravel-datao similares, y aplica la forma de las respuestas mediante tipos
4. Versionado y garantías de compatibilidad
4.1 Comparación de enfoques
- Prefijo URI:
/api/v1/...es lo más fácil de entender; bueno para enrutamiento y caché - Cabecera (
Accept: application/vnd.example.v2+json): elegante pero con mayor coste de aprendizaje - Subdominio:
v2.api.example.comfunciona bien con gateways de API
Punto de partida recomendado: /api/v1.
Cuando introduzcas cambios incompatibles, exponlos como v2 y ejecuta v1 y v2 en paralelo durante un tiempo.
4.2 Categorías de cambio y operación
-
No incompatibles
- Añadir campos
- Relajar valores por defecto
- Aumentar límites superiores (por ejemplo, tamaño máximo de página)
-
Suavemente incompatibles (requieren anuncio, pero pueden no romper todos los clientes)
- Cambiar valores por defecto
- Cambiar el orden de clasificación
-
Incompatibles (breaking)
- Eliminar campos
- Cambiar tipos
- Cambiar semánticas
- Eliminar endpoints
Regla por defecto: evita cambios incompatibles y pregúntate «¿Se puede expresar esto como un cambio aditivo?»
Si un cambio incompatible es realmente necesario, publica una nueva versión, proporciona una guía de migración detallada y un linter/comprobador de compatibilidad.
5. Autenticación, autorización y scopes
5.1 Métodos de autenticación
- Sesión + CSRF: para SPA de mismo origen
- Tokens de acceso personales de Sanctum: para clientes externos / APIs
- OAuth 2.1 / OIDC (IdP externo): para integraciones con socios o casos de uso de SSO en SaaS
5.2 Scopes (abilities)
// emisión
$token = $user->createToken('cli', ['orders:read', 'orders:write'])->plainTextToken;
// comprobación
abort_unless($request->user()->tokenCan('orders:write'), 403);
Scopes más granulares mejoran la seguridad, pero complican la operación.
Empieza de forma sencilla con una división lectura / escritura, y refínala según se clarifiquen las necesidades.
6. Enrutamiento, nombres, paginación y ordenación
6.1 Nombres
- Usa nombres de recurso en plural:
GET /orders,GET /orders/{id},POST /orders
- Representa acciones como subrecursos:
POST /orders/{id}/cancelpara que la intención sea explícita
- La consistencia gana a la ingeniosidad: mejor corto y concreto
6.2 Paginación, ordenación y filtrado
GET /orders?page=2&per_page=50&sort=-created_at&status=shipped
Pautas:
- Establece un máximo para
per_page(por ejemplo, 100) - Permite ordenar solo por una lista blanca de campos; rechaza columnas arbitrarias
- Incluye información de paginación en la respuesta
{
"data": [ ... ],
"meta": { "total": 1234, "page": 2, "per_page": 50 },
"links": { "next": "...", "prev": "..." }
}
7. Peticiones condicionales y caché HTTP
7.1 ETag / If-None-Match
Añade un hash (ETag) a cada representación de recurso. Cuando el cliente envía If-None-Match, devuelve 304 Not Modified si no hay cambios.
public function show(Order $order) {
$etag = sha1($order->updated_at . $order->id);
if (request()->header('If-None-Match') === $etag) {
return response()->noContent(304)->header('ETag', $etag);
}
return response()
->json($order)
->header('ETag', $etag);
}
7.2 Last-Modified / If-Modified-Since
Las peticiones condicionales basadas en tiempo también son útiles. Reducir las recargas innecesarias ahorra tanto costes como latencia.
8. Limitación de tasa, idempotencia y reintentos
8.1 Limitación de tasa
// routes/api.php
Route::middleware('throttle:60,1')->group(function () {
Route::get('/orders', [OrderController::class, 'index']);
});
Pautas:
- Utiliza claves de API o IDs de usuario como clave (“por”) según el caso de uso para aplicar equidad
- Devuelve
429 Too Many RequestsconRetry-Aftercuando se limite
8.2 Idempotencia (evitar efectos secundarios duplicados)
Para pagos y operaciones de tipo creación, usa claves de idempotencia para evitar ejecuciones duplicadas.
// Esbozo de middleware
$key = request()->header('Idempotency-Key');
abort_unless($key, 400);
$lock = Cache::lock("idem:$key", 60);
abort_unless($lock->get(), 409); // ya en curso
try {
// Si ya tenemos una respuesta para esta clave, la reproducimos
if ($cached = Cache::get("idem:resp:$key")) {
return response()->json($cached['body'], $cached['status']);
}
$resp = $next($request);
Cache::put(
"idem:resp:$key",
[
'status' => $resp->status(),
'body' => json_decode($resp->getContent(), true),
],
3600
);
return $resp;
} finally {
optional($lock)->release();
}
8.3 Estrategias de reintento
Los clientes deberían usar backoff exponencial para reintentos (por ejemplo, 100 ms, 200 ms, 400 ms, …).
En el lado del servidor, diseña endpoints para ser seguros en re-ejecuciones.
9. Diseño de errores: estandarizar en RFC 7807 (problem+json)
9.1 Formato
{
"type": "https://docs.example.com/problems/validation",
"title": "Validation Failed",
"status": 422,
"detail": "The email field is required.",
"instance": "/api/v1/users",
"errors": {
"email": ["This field is required."]
},
"trace_id": "req-8a2c..."
}
Convenciones:
type: URL a una página de documentación fija, con pasos reproducidos y correcciones recomendadas- Incluye siempre un
trace_id, para que soporte y operaciones puedan localizar rápidamente los logs
9.2 Implementación en Laravel
Captura Throwable en el exception handler y mapea a respuestas problem+json según los códigos de estado. Unifica los errores de validación de FormRequest en el mismo formato.
// app/Exceptions/Handler.php (extracto)
public function render($request, Throwable $e)
{
if ($request->expectsJson()) {
$traceId = (string) Str::uuid();
Log::error('api.error', ['trace_id' => $traceId, 'ex' => $e]);
$status = $this->statusOf($e); // mapear excepción a código HTTP
$payload = [
'type' => $this->problemType($e),
'title' => Response::$statusTexts[$status] ?? 'Error',
'status' => $status,
'detail' => $this->detailOf($e),
'instance' => $request->path(),
'trace_id' => $traceId,
];
if ($e instanceof ValidationException) {
$payload['type'] = 'https://docs.example.com/problems/validation';
$payload['errors'] = $e->errors();
}
return response()
->json($payload, $status)
->header('Content-Type', 'application/problem+json');
}
return parent::render($request, $e);
}
9.3 Mensajes multilingües
Localiza detail según Accept-Language.
No cambies los campos legibles por máquina type y status. Proporcionar una forma de acceder a un detail en inglés también puede ayudar a los equipos de soporte.
10. Asegurar y dimensionar payloads JSON
- Usa
application/jsony desactiva JSONP - Devuelve solo los campos mínimos necesarios. No expongas información interna para observabilidad (stack traces, PII, etc.)
- Pagina siempre las listas. Usa límites en
per_pagepara controlar el ancho de banda - Para arrays enormes, considera el streaming o exports asíncronos
11. Webhooks: verificación de firma y protección contra reenvíos
11.1 Como emisor (tú envías webhooks)
- Incluye una firma y marca de tiempo en las cabeceras
- Incluye una clave de idempotencia (ID de evento)
$payload = json_encode($event, JSON_UNESCAPED_UNICODE);
$ts = time();
$sig = hash_hmac('sha256', $ts . '.' . $payload, config('services.webhook.secret'));
Http::withHeaders([
'X-Webhook-Timestamp' => $ts,
'X-Webhook-Signature' => $sig,
'Idempotency-Key' => $event['id'],
])->post($url, $event);
11.2 Como receptor (recibes webhooks externos)
- Primero verifica la firma y la marca de tiempo (con una tolerancia, por ejemplo, ±5 minutos)
- Tras la verificación, delega el procesamiento a un job. Ignora IDs de evento duplicadas.
$payload = $request->getContent();
$ts = $request->header('X-Webhook-Timestamp');
$sig = $request->header('X-Webhook-Signature');
abort_if(abs(time() - (int) $ts) > 300, 401); // marca de tiempo demasiado antigua
$calc = hash_hmac('sha256', $ts . '.' . $payload, config('services.webhook.secret'));
abort_unless(hash_equals($calc, $sig), 401);
$event = json_decode($payload, true);
if (Cache::add('evt:' . $event['id'], true, 3600)) {
dispatch(new HandleWebhook($event));
}
12. Accesibilidad de la documentación: fácil de leer, fácil de encontrar, fácil de probar
12.1 Arquitectura de información
- Proporciona una tabla de contenidos en una barra lateral izquierda, navegable con teclado
- Mantén un tema por página. En la parte superior, muestra: “Qué puedes hacer”, “Para quién es” y “Camino más corto hacia el éxito”
- No dependas solo de colores de fondo o insignias. Usa etiquetas de texto e iconos para los estados
- Proporciona texto alternativo y descripciones más largas para diagramas y diagramas de secuencia
12.2 Creación de ejemplos
- Proporciona al menos estas cuatro variantes:
- cURL
- JavaScript (fetch/axios)
- PHP (cliente HTTP)
- Python (requests)
- Asegúrate de que el botón de copia sea accesible con teclado
- El código largo debe poder plegarse y tener siempre encabezados y explicaciones
- Incluye ejemplos de fallo (401 / 403 / 422 / 429 / 500) y una breve guía sobre cómo corregir cada caso
12.3 Catálogo de errores
Publica una página por cada type con una URL fija.
Describe:
- Causa
- Cómo corregir
- Condiciones de reproducción
- Si es correcto reintentar
- Contacto de soporte
Usa el color solo como complemento; el texto debe ser lo principal.
13. Ejemplo de implementación: Orders API (extracto)
13.1 Rutas
Route::prefix('api/v1')
->middleware(['auth:sanctum', 'throttle:120,1'])
->group(function () {
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store'])
->middleware('idem.key'); // idempotencia
Route::get('/orders/{order}', [OrderController::class, 'show']);
Route::post('/orders/{order}/cancel', [OrderController::class, 'cancel']);
});
13.2 Validación
class StoreOrderRequest extends FormRequest
{
public function rules(): array
{
return [
'items' => ['required', 'array', 'min:1'],
'items.*.sku' => ['required', 'string'],
'items.*.qty' => ['required', 'integer', 'min:1', 'max:100'],
'note' => ['nullable', 'string', 'max:500'],
];
}
}
13.3 Controlador (soporte de ETag en show)
public function show(Order $order)
{
$this->authorize('view', $order);
$etag = sha1($order->updated_at . $order->id);
if (request()->header('If-None-Match') === $etag) {
return response()
->noContent(304)
->header('ETag', $etag);
}
return response()
->json([
'data' => [
'id' => $order->id,
'status' => $order->status,
'total' => $order->total,
'items' => $order->items()->get(['sku', 'name', 'qty', 'price']),
],
'meta' => ['currency' => 'JPY'],
], 200)
->header('ETag', $etag);
}
13.4 Error (422 como problem+json)
throw ValidationException::withMessages([
'items.0.sku' => ['Este SKU no existe.'],
]);
14. Pruebas de contrato y comprobaciones de compatibilidad
14.1 Pruebas de contrato
- Carga la OAS y, para cada endpoint, verifica que las respuestas reales cumplan la especificación
- Detecta en CI cambios de esquema incompatibles hacia atrás (cambios de tipo, campos que pasan a ser obligatorios, etc.)
14.2 Prevención de regresiones
- Ejecuta pruebas E2E de humo para clientes representativos (SDKs internos) en cada despliegue
- Utiliza
trace_idpara acortar significativamente las investigaciones cuando se produzcan incidentes
15. Monitorización y SLOs
Haz seguimiento de:
- Latencia p50 / p95 / p99, tasas de error (5xx / 4xx), tasa de impactos de limitación de tasa, ratio de aciertos de caché
- Retrasos de colas en el backend, consultas lentas en la base de datos
Define SLOs para los endpoints críticos (por ejemplo, p95 < 300 ms, 5xx < 0,1 %) y configura alertas cuando se violen.
16. Puntos clave para el endurecimiento de seguridad
- Usa HSTS, CSP,
X-Content-Type-Options: nosniff,Referrer-Policy - Validación de entrada (
FormRequest) y protección contra inyección SQL / de plantillas / de comandos - Para subidas de archivos, aplica comprobaciones de MIME, límites de tamaño y análisis antivirus
- Logs de auditoría: almacena
trace_id,user_id,ip,path,statusylatencycomo logs estructurados - Enmascara datos sensibles (tokens, datos de tarjeta, identificadores personales)
17. Aplicación de ejemplo accesible y portal para desarrolladores
- La navegación del portal debe ser totalmente usable con teclado, con anillos de enfoque visibles
- Los bloques de código deberían tener etiquetas de idioma y un botón de copiar, con esquemas de color que funcionen para personas daltónicas
- Las tablas y definiciones de parámetros deben tener encabezados y descripciones claros; no te apoyes solo en iconos
- Para solicitudes de ejemplo, muestra el mismo contenido mediante curl y varios lenguajes, y proporciona respuestas bien formateadas
- Ofrece modo oscuro y asegúrate de que el contraste sea suficiente (WCAG AA o superior)
18. Errores habituales y cómo evitarlos
-
Pasar parámetros de ordenación/filtrado arbitrarios directamente a SQL
- ➜ Permite solo campos de una lista blanca y valida los valores
-
Números de versión “falsos” sin una política real de compatibilidad
- ➜ Empareja el versionado con una regla para evitar cambios incompatibles más guías de migración
-
Devolver listas completas sin paginar
- ➜ Obliga a paginar y usa ETag/peticiones condicionales para reducir los costes de recarga
-
Formatos de error inconsistentes
- ➜ Estandariza en RFC 7807 y enlaza a un catálogo de errores
-
Endpoints de creación sin idempotencia
- ➜ Implementa
Idempotency-Keyy bloqueo/caché en el servidor
- ➜ Implementa
-
Webhooks sin protección frente a suplantación
- ➜ Verifica firmas, aplica ventanas de tiempo para timestamps e ignora IDs de evento duplicados
-
Documentación que depende solo del color
- ➜ Haz que el texto sea lo principal, usa texto alternativo y asegúrate de que los flujos puedan completarse solo con el teclado
-
Incidentes imposibles de rastrear
- ➜ Devuelve
trace_iden cada respuesta y relaciónalo con los logs
- ➜ Devuelve
19. Lista de comprobación (para distribución)
Diseño
- [ ] Estilo de API elegido (REST / JSON:API / GraphQL) y reglas documentadas para desviaciones
- [ ] OpenAPI es la única fuente de verdad; las pruebas de contrato están conectadas al CI
- [ ] Versionado con
/api/v{n}y política escrita para cambios incompatibles y migración
Funcionalidad
- [ ] Autenticación (Sanctum/OAuth) con scopes de mínimo privilegio
- [ ] Paginación / ordenación / filtrado mediante parámetros de lista blanca y límites sensatos
- [ ] ETag / peticiones condicionales para reducir ancho de banda
- [ ] Limitación de tasa, idempotencia y política de reintentos definidas
Errores
- [ ] RFC 7807 estandarizado,
trace_idsiempre incluido - [ ]
detailmultilingüe y URLs para las páginas del catálogo de errores
Seguridad / Operaciones
- [ ] Webhooks firmados con protección frente a reenvíos
- [ ] Logs de auditoría estructurados con PII enmascarada
- [ ] SLOs y paneles de métricas en producción
Documentación / Accesibilidad
- [ ] Tabla de contenidos, navegación por teclado, contraste y texto alternativo garantizados
- [ ] Ejemplos en cURL + varios lenguajes con botón de copia
- [ ] Ejemplos de error y cómo solucionarlos documentados
20. Conclusión
Las APIs son infraestructuras públicas de larga vida.
Centra tu diseño en OpenAPI, preserva la compatibilidad, unifica las representaciones de error y utiliza caché y limitación de tasa para mantener sana la plataforma. Haz que los endpoints de tipo creación sean robustos frente a duplicados con idempotencia. Para webhooks, combina verificación de firma con protección frente a reenvíos.
La documentación debería ser fácil de leer y de probar para todo el mundo: no dependas solo de colores o imágenes; transmite el significado mediante texto y estructura.
Utiliza los diseños y ejemplos de este artículo como punto de partida para establecer los estándares de API de tu equipo. Si cuidas la plataforma, puede llegar a ser amable tanto para usuarios como para desarrolladores.
Referencias
-
Documentación oficial de Laravel
-
Especificaciones / estándares
-
Seguridad / Operaciones
-
Documentación / Accesibilidad
