Diseño de REST API en Laravel: principios, versionado y documentación en 2026
Introducción: por qué el diseño de la API importa más que su implementación
La mayoría de los desarrolladores escriben el código primero y piensan en la arquitectura después. Con las REST API, este enfoque es especialmente destructivo: las malas decisiones en la fase de diseño se convierten en deuda técnica que es casi imposible de saldar sin introducir breaking changes.
En 2026, Laravel sigue siendo uno de los frameworks PHP más populares para crear backends de API. Su ecosistema es maduro y sus herramientas son ricas, pero eso no elimina la necesidad de pensar antes de escribir. Este artículo trata sobre cómo diseñar una REST API en Laravel correctamente desde el primer intento.
Principios del diseño RESTful
Recursos y nomenclatura de rutas
REST se construye en torno a recursos, no a acciones. La URL debe representar un sustantivo y el método HTTP debe actuar como el verbo.
- Correcto:
GET /api/v1/articles - Incorrecto:
GET /api/v1/getArticles
Usa el plural para las colecciones (/users), rutas anidadas para las relaciones (/users/42/posts), y evita anidar más de dos niveles, ya que complica el código del cliente.
Métodos HTTP y su semántica
GET— obtención de un recurso o colección (seguro, idempotente)POST— creación de un nuevo recursoPUT— actualización completa del recurso (idempotente)PATCH— actualización parcialDELETE— eliminación del recurso (idempotente)
Códigos de estado HTTP
El uso correcto de los códigos de estado es señal de una API madura. No devuelvas 200 OK en cada solicitud con un mensaje de error en el cuerpo.
200— GET/PUT/PATCH exitoso201— POST exitoso (recurso creado)204— DELETE exitoso (cuerpo vacío)400— error de validación401— no autenticado403— sin permisos de acceso404— recurso no encontrado422— Unprocessable Entity (Laravel lo usa para validación)500— error interno del servidor
Idempotencia
Una solicitud idempotente produce el mismo resultado al ejecutarse varias veces. PUT /users/1 con los mismos datos no debe crear duplicados. Esto es fundamental para la fiabilidad en redes inestables.
Estructura del proyecto Laravel para API
Rutas (Routes)
Todas las rutas de la API se ubican en routes/api.php. Laravel agrega automáticamente el prefijo /api y aplica el middleware api.
// routes/api.php\nuse App\\Http\\Controllers\\Api\\V1\\ArticleController;\n\nRoute::prefix('v1')->middleware('auth:sanctum')->group(function () {\n Route::apiResource('articles', ArticleController::class);\n Route::apiResource('users.posts', PostController::class)->shallow();\n});Controladores (Controllers)
Usa el flag --api al generar controladores: crea únicamente los métodos necesarios, sin create ni edit.
php artisan make:controller Api/V1/ArticleController --api --model=ArticleEjemplo de controlador utilizando API Resources:
<?php\n\nnamespace App\\Http\\Controllers\\Api\\V1;\n\nuse App\\Http\\Controllers\\Controller;\nuse App\\Http\\Requests\\StoreArticleRequest;\nuse App\\Http\\Requests\\UpdateArticleRequest;\nuse App\\Http\\Resources\\ArticleResource;\nuse App\\Models\\Article;\n\nclass ArticleController extends Controller\n{\n public function index()\n {\n $articles = Article::with('author')\n ->latest()\n ->paginate(15);\n\n return ArticleResource::collection($articles);\n }\n\n public function store(StoreArticleRequest $request)\n {\n $article = Article::create($request->validated());\n\n return new ArticleResource($article);\n }\n\n public function show(Article $article)\n {\n return new ArticleResource($article->load('author', 'tags'));\n }\n\n public function update(UpdateArticleRequest $request, Article $article)\n {\n $article->update($request->validated());\n\n return new ArticleResource($article);\n }\n\n public function destroy(Article $article)\n {\n $article->delete();\n\n return response()->noContent();\n }\n}API Resources
Nunca devuelvas modelos Eloquent directamente. Los API Resources son la capa de transformación de datos que protege contra la exposición accidental de campos y permite modificar la estructura de la respuesta de forma independiente al modelo.
<?php\n\nnamespace App\\Http\\Resources;\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Resources\\Json\\JsonResource;\n\nclass ArticleResource extends JsonResource\n{\n public function toArray(Request $request): array\n {\n return [\n 'id' => $this->id,\n 'title' => $this->title,\n 'slug' => $this->slug,\n 'content' => $this->content,\n 'published' => $this->published_at?->toIso8601String(),\n 'author' => new UserResource($this->whenLoaded('author')),\n 'tags' => TagResource::collection($this->whenLoaded('tags')),\n 'created_at' => $this->created_at->toIso8601String(),\n ];\n }\n}Form Requests
Extrae la validación a Form Requests: esto aligera los controladores y permite reutilizar las reglas:
<?php\n\nnamespace App\\Http\\Requests;\n\nuse Illuminate\\Foundation\\Http\\FormRequest;\n\nclass StoreArticleRequest extends FormRequest\n{\n public function authorize(): bool\n {\n return $this->user()->can('create', Article::class);\n }\n\n public function rules(): array\n {\n return [\n 'title' => ['required', 'string', 'max:255'],\n 'content' => ['required', 'string'],\n 'tags' => ['array'],\n 'tags.*' => ['integer', 'exists:tags,id'],\n ];\n }\n}Versionado de API: URI versioning vs Header versioning
Una de las preguntas más debatidas en el diseño de API. Existen dos enfoques principales:
URI Versioning
GET /api/v1/articles\nGET /api/v2/articlesVentajas: es visual, fácil de probar en el navegador y claro para el caché a nivel de CDN. Desventajas: técnicamente, la URL no debería contener información sobre la versión del protocolo.
Header Versioning
GET /api/articles\nAccept: application/vnd.myapp.v2+jsonVentajas: más "puro" desde el punto de vista del purismo REST. Desventajas: más difícil de depurar y funciona mal con las herramientas del navegador.
Recomendación para 2026: usa URI versioning para APIs públicas y de socios: es la elección pragmática de la gran mayoría de las empresas importantes (GitHub, Stripe, Twilio). El Header versioning solo se justifica si controlas todos los clientes y puedes garantizar la transmisión correcta de los encabezados.
En Laravel, el versionado mediante URI se organiza con la estructura de carpetas:
app/Http/Controllers/Api/V1/ArticleController.php\napp/Http/Controllers/Api/V2/ArticleController.php\n\nroutes/api/v1.php\nroutes/api/v2.phpAutenticación y autorización: Laravel Sanctum vs Passport en 2026
Laravel Sanctum
Sanctum es la solución recomendada para la mayoría de los proyectos en 2026. Soporta tokens de API y autenticación SPA mediante sesiones de cookie. Es sencillo de configurar y no requiere un servidor OAuth.
// Instalación\ncomposer require laravel/sanctum\nphp artisan vendor:publish --provider=\"Laravel\\Sanctum\\SanctumServiceProvider\"\nphp artisan migrate\n\n// Emisión de token\n$token = $user->createToken('mobile-app', ['articles:read', 'articles:write']);\nreturn ['token' => $token->plainTextToken];\n\n// Verificación de capacidades del token\nif ($request->user()->tokenCan('articles:write')) {\n // ...\n}Laravel Passport
Passport implementa un servidor OAuth 2.0 completo. Es necesario cuando emites tokens a aplicaciones de terceros, se requiere el Authorization Code Flow, o estás construyendo una plataforma con API para socios.
Conclusión: si tienes una aplicación móvil o SPA, usa Sanctum. Si estás construyendo un proveedor OAuth para clientes externos, usa Passport.
Manejo de errores y estandarización de respuestas
Un formato de respuesta unificado es fundamental para los desarrolladores cliente. Configura el manejador global de excepciones en app/Exceptions/Handler.php:
<?php\n\nuse Illuminate\\Auth\\AuthenticationException;\nuse Illuminate\\Validation\\ValidationException;\nuse Symfony\\Component\\HttpKernel\\Exception\\HttpException;\n\n// En el método register():\n$this->renderable(function (\\Throwable $e, $request) {\n if ($request->expectsJson()) {\n if ($e instanceof ValidationException) {\n return response()->json([\n 'message' => 'Validation failed',\n 'errors' => $e->errors(),\n ], 422);\n }\n\n if ($e instanceof AuthenticationException) {\n return response()->json([\n 'message' => 'Unauthenticated.',\n ], 401);\n }\n\n if ($e instanceof HttpException) {\n return response()->json([\n 'message' => $e->getMessage() ?: 'HTTP error',\n ], $e->getStatusCode());\n }\n\n return response()->json([\n 'message' => 'Server error',\n ], 500);\n }\n});La respuesta exitosa estándar con paginación incluye automáticamente los campos data, links y meta cuando se usa Resource::collection() con paginación — aprovecha esto.
Documentación de API con Scribe o L5-Swagger
Scribe
Scribe es una herramienta moderna de generación automática de documentación para Laravel. Analiza rutas, Form Requests y comentarios docblock, generando documentación HTML elegante y una especificación OpenAPI.
composer require --dev knuckleswtf/scribe\nphp artisan vendor:publish --tag=scribe-config\nphp artisan scribe:generateAnota los controladores para mejorar la documentación:
/**\n * @group Articles\n *\n * API para gestionar artículos\n */\nclass ArticleController extends Controller\n{\n /**\n * Listado de artículos\n *\n * Devuelve una lista paginada de todos los artículos publicados.\n *\n * @queryParam page integer Número de página. Example: 1\n * @queryParam per_page integer Cantidad por página (máx. 50). Example: 15\n */\n public function index() { ... }\n}L5-Swagger (darkaonline/l5-swagger)
Si el equipo prefiere OpenAPI 3.0 y Swagger UI, L5-Swagger sigue siendo una buena opción. Usa atributos PHP en lugar de anotaciones en comentarios: es más seguro en tipos y tiene soporte en IDEs.
composer require darkaonline/l5-swagger\nphp artisan vendor:publish --provider \"L5Swagger\\L5SwaggerServiceProvider\"\nphp artisan l5-swagger:generateConsejo: integra la generación de documentación en el pipeline de CI/CD para que la documentación siempre esté sincronizada con el código.
Pruebas de REST API en Laravel
Una API bien probada es una API en la que se puede confiar. Laravel ofrece herramientas potentes para Feature tests desde el primer momento.
<?php\n\nnamespace Tests\\Feature\\Api\\V1;\n\nuse App\\Models\\Article;\nuse App\\Models\\User;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse Tests\\TestCase;\n\nclass ArticleApiTest extends TestCase\n{\n use RefreshDatabase;\n\n public function test_authenticated_user_can_create_article(): void\n {\n $user = User::factory()->create();\n\n $response = $this->actingAs($user, 'sanctum')\n ->postJson('/api/v1/articles', [\n 'title' => 'Test Article',\n 'content' => 'Some content here',\n ]);\n\n $response\n ->assertStatus(201)\n ->assertJsonStructure([\n 'data' => ['id', 'title', 'slug', 'content', 'created_at'],\n ])\n ->assertJsonPath('data.title', 'Test Article');\n\n $this->assertDatabaseHas('articles', ['title' => 'Test Article']);\n }\n\n public function test_unauthenticated_request_returns_401(): void\n {\n $this->postJson('/api/v1/articles', ['title' => 'Test'])\n ->assertStatus(401);\n }\n\n public function test_validation_returns_422_with_errors(): void\n {\n $user = User::factory()->create();\n\n $this->actingAs($user, 'sanctum')\n ->postJson('/api/v1/articles', [])\n ->assertStatus(422)\n ->assertJsonValidationErrors(['title', 'content']);\n }\n\n public function test_article_list_is_paginated(): void\n {\n $user = User::factory()->create();\n Article::factory()->count(20)->create();\n\n $this->actingAs($user, 'sanctum')\n ->getJson('/api/v1/articles')\n ->assertStatus(200)\n ->assertJsonStructure([\n 'data', 'links', 'meta' => ['total', 'per_page', 'current_page'],\n ]);\n }\n}Ejecuta las pruebas con el flag --parallel para acelerar el proceso en CI:
php artisan test --parallel --coverageConclusión: checklist de una API lista para producción
Antes de considerar la API lista para producción, revisa esta lista:
- Rutas: usan sustantivos en plural y están versionadas por URI (
/api/v1/) - Métodos HTTP: se usan según su semántica — GET, POST, PUT/PATCH, DELETE
- Códigos de estado: 201 al crear, 204 al eliminar, 422 en validación, 401/403 en errores de autenticación
- API Resources: todas las respuestas pasan por transformadores Resource, los modelos no se devuelven directamente
- Form Requests: la validación y autorización están extraídas de los controladores
- Autenticación: Sanctum configurado (o Passport para OAuth) con los scopes/abilities correctos
- Manejo de errores: el handler global devuelve respuestas JSON para todas las excepciones
- Versionado: rutas y controladores están estructurados por versiones
- Documentación: generada con Scribe o L5-Swagger e integrada en CI
- Pruebas: cubren el happy path, errores de validación y escenarios de autenticación
- Rate Limiting: configurado mediante
RateLimiter::for()enRouteServiceProvider - Problemas N+1: eliminados mediante eager loading; Laravel Debugbar o Telescope conectados para monitoreo
Una REST API de calidad en Laravel no es solo código correcto, sino también una arquitectura bien pensada, un comportamiento predecible y una buena documentación. Invierte tiempo en el diseño: se amortizará con creces al escalar y trabajar en equipo.
Tecnologías
Etiquetas
Ruslan Ismailov
Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →