Desarrollo backend

Diseño de REST API en Laravel: principios, versionado y documentación en 2026

Ruslan Ismailov Publicado 11 min de lectura
D

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 recurso
  • PUT — actualización completa del recurso (idempotente)
  • PATCH — actualización parcial
  • DELETE — 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 exitoso
  • 201 — POST exitoso (recurso creado)
  • 204 — DELETE exitoso (cuerpo vacío)
  • 400 — error de validación
  • 401 — no autenticado
  • 403 — sin permisos de acceso
  • 404 — recurso no encontrado
  • 422 — 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=Article

Ejemplo 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/articles

Ventajas: 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+json

Ventajas: 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.php

Autenticació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:generate

Anota 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:generate

Consejo: 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 --coverage

Conclusión: checklist de una API lista para producción

Antes de considerar la API lista para producción, revisa esta lista:

  1. Rutas: usan sustantivos en plural y están versionadas por URI (/api/v1/)
  2. Métodos HTTP: se usan según su semántica — GET, POST, PUT/PATCH, DELETE
  3. Códigos de estado: 201 al crear, 204 al eliminar, 422 en validación, 401/403 en errores de autenticación
  4. API Resources: todas las respuestas pasan por transformadores Resource, los modelos no se devuelven directamente
  5. Form Requests: la validación y autorización están extraídas de los controladores
  6. Autenticación: Sanctum configurado (o Passport para OAuth) con los scopes/abilities correctos
  7. Manejo de errores: el handler global devuelve respuestas JSON para todas las excepciones
  8. Versionado: rutas y controladores están estructurados por versiones
  9. Documentación: generada con Scribe o L5-Swagger e integrada en CI
  10. Pruebas: cubren el happy path, errores de validación y escenarios de autenticación
  11. Rate Limiting: configurado mediante RateLimiter::for() en RouteServiceProvider
  12. 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í →