Guía práctica completa: diseño de API Resources en Laravel — organizar respuestas JSON, DTOs, paginación, errores unificados e integración frontend
Lo que aprenderás en este artículo
- Cómo usar Laravel API Resources para mantener el diseño de respuestas claro y mantenible
- Cómo separar responsabilidades con Resource / DTO / Action en lugar de devolver modelos Eloquent directamente
- Patrones prácticos para listas, detalles, datos anidados, campos condicionales y paginación
- Principios de diseño JSON que funcionan bien con aplicaciones frontend y móviles
- Cómo unificar errores de validación, errores de autorización y respuestas de excepción
- Cómo hacer crecer APIs fáciles de probar y resistentes a cambios futuros
- Cómo un diseño de API organizado conduce a documentación más clara tanto para lectores como para implementadores
Lectores previstos
- Ingenieros Laravel de nivel principiante a intermedio que pueden construir APIs, pero tienen dificultades con formatos de respuesta inconsistentes
- Líderes técnicos que quieren estandarizar el diseño JSON para integración móvil y frontend
- Equipos de QA y mantenimiento que quieren reducir errores causados por cambios en la estructura de respuesta
- Diseñadores y desarrolladores frontend que quieren estructuras de datos estables para implementación de UI y gestión de estado
Nivel de accesibilidad: ★★★★☆
Aunque este artículo se centra en el diseño de respuestas backend, los datos de respuesta bien estructurados facilitan que los equipos frontend implementen encabezados, mensajes de estado y guías de error consistentes. Como resultado, apoyan expresiones de estado que no dependen solo del color, notificaciones comprensibles para lectores de pantalla y vistas estables de lista/detalle.
1. Introducción: las APIs no deberían simplemente “devolver datos”; deberían ser fáciles de leer y cambiar
Cuando empiezas a construir APIs con Laravel, es tentador devolver modelos Eloquent directamente, como return User::all();. Esto sí devuelve JSON, y al principio es rápido. Sin embargo, a medida que las funciones crecen, comienzan a aparecer gradualmente los siguientes problemas.
- Diferentes pantallas necesitan diferentes campos, lo que provoca formatos de respuesta inconsistentes
- Los campos internos del modelo pueden exponerse tal cual, difuminando responsabilidades
- Las estructuras JSON de lista y detalle son inconsistentes, lo que dificulta el trabajo frontend
- La paginación y las estructuras anidadas se vuelven ad hoc y difíciles de organizar más tarde
- Se vuelve difícil predecir el impacto de cambiar nombres de campos en el futuro
- Las pruebas de API se vuelven difíciles de escribir, haciendo que los cambios se sientan arriesgados
Laravel proporciona API Resources para reducir estos problemas. Con API Resources, en lugar de “mostrar el contenido del modelo tal cual”, defines explícitamente “cómo deben mostrarse los datos hacia el exterior”. Esto puede parecer modesto, pero es muy importante. Una vez que las formas de respuesta se gestionan en código, el contrato con el frontend se vuelve estable y la API se vuelve más resistente a cambios futuros.
2. ¿Qué es un API Resource? Una capa que define cómo los modelos se presentan como respuestas
Un API Resource es una capa de formateo para respuestas JSON en Laravel.
En términos simples, su rol es el siguiente.
- Model: Los datos en sí
- Action / Service: Procesamiento de negocio
- Resource: Formateo de datos para presentación externa
- Controller: Coordinación de entrada y salida HTTP
Con esta separación, los modelos pueden centrarse en los datos y las relaciones, mientras que la presentación de API se centraliza en Resources. Por ejemplo, ya no necesitas meter “nombres de visualización para API”, “estructuras solo para pantalla de detalle” y “JSON de pantalla administrativa” dentro del propio modelo User.
Laravel API Resources se basan en JsonResource para modelos individuales y Resource Collections para listas. Al principio, esto puede sentirse ligeramente indirecto, pero cuantas más pantallas tienes, más rinde esta capa adicional.
3. Empieza con lo básico: crear un Resource individual simple
Como ejemplo, consideremos una API que devuelve datos de publicaciones.
Primero, crea un Resource.
php artisan make:resource PostResource
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => (string) $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'published_at' => optional($this->published_at)?->toIso8601String(),
'author_name' => $this->user?->name,
];
}
}
En el controlador, devuélvelo así.
use App\Http\Resources\PostResource;
use App\Models\Post;
public function show(Post $post): PostResource
{
$post->loadMissing('user');
return new PostResource($post);
}
Incluso en esta etapa, ya obtienes beneficios importantes.
- Puedes unificar políticas como devolver
idcomo string - Puedes estandarizar formatos de fecha en el Resource
- Puedes evitar exponer campos que existen en el modelo pero no deberían ser públicos
- Puedes dar forma a campos como
author_nameen un formato amigable para el frontend
En comparación con devolver Eloquent directamente, el contrato de la API se vuelve mucho más claro.
4. Por qué los Resources mejoran la mantenibilidad: la responsabilidad de respuesta se centraliza
En la práctica, dónde ocurre el formateo importa mucho.
Sin Resources, el formateo tiende a dispersarse en muchos lugares.
- Usar
->map()dentro de controladores - Añadir accessors a modelos
- Construir arrays dentro de servicios
- Devolver diferentes formatos pantalla por pantalla
Cuando esto ocurre, se vuelve difícil saber dónde editar cuando la forma de la API necesita cambiar. Al introducir Resources, puedes hacer que el Resource sea el lugar donde buscar los formatos de respuesta externos. Esto ayuda en las revisiones de código y también facilita las conversaciones con desarrolladores frontend.
Por ejemplo, cuando alguien dice: “La lista no necesita el cuerpo completo”, o “La página de detalle necesita etiquetas e información del autor”, se vuelve más fácil decidir si crear un Resource separado o usar campos condicionales.
En otras palabras, un Resource no es simplemente formateo de arrays. Funciona como un documento de diseño para la API.
5. Piensa en las listas como Resource Collections: separa responsabilidades individuales y de lista
Las APIs de lista necesitan más que datos individuales. A menudo necesitan lo siguiente.
- Array de datos
- Conteo total
- Página actual
- Si hay una página siguiente
- Estado actual de filtros
En Laravel, aplicar PostResource::collection($posts) a resultados paginados suele darte una estructura limpia.
use App\Http\Resources\PostResource;
use App\Models\Post;
public function index(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection
{
$posts = Post::query()
->select(['id', 'user_id', 'title', 'slug', 'excerpt', 'published_at'])
->with(['user:id,name'])
->published()
->latest()
->paginate(20);
return PostResource::collection($posts);
}
Esto devuelve una estructura que incluye data, links y meta.
En muchos casos, puedes usar el mismo Resource tanto para vistas de lista como de detalle. Sin embargo, en la práctica, a menudo es más claro separar un “Resource de lista” y un “Resource de detalle”.
Por ejemplo, las listas deberían ser ligeras, mientras que los detalles pueden ser más ricos.
5.1 Ejemplo: separar un Resource específico para lista
class PostListResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => (string) $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'author_name' => $this->user?->name,
'published_at' => optional($this->published_at)?->toDateString(),
];
}
}
Esto evita devolver datos anidados innecesarios o texto de cuerpo grande en respuestas de lista.
6. Manejo de datos anidados: no devuelvas modelos relacionados directamente; usa Resources por capas
A medida que las APIs crecen, a menudo quieres incluir datos relacionados. Un error común es insertar modelos relacionados directamente en el array de respuesta. Pero esto vuelve a traer la exposición del modelo. Usar Resources para modelos relacionados mantiene las cosas limpias.
class UserSummaryResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => (string) $this->id,
'name' => $this->name,
];
}
}
class PostDetailResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => (string) $this->id,
'title' => $this->title,
'slug' => $this->slug,
'body' => $this->body,
'published_at' => optional($this->published_at)?->toIso8601String(),
'author' => new UserSummaryResource($this->whenLoaded('user')),
];
}
}
Usar whenLoaded() es muy importante en la práctica.
Evita que el Resource toque inesperadamente relaciones que no fueron cargadas en el controlador, ayudando a evitar la aparición de problemas de consultas N+1.
7. Campos condicionales: organiza valores visibles solo para administradores o pantallas de detalle
En las APIs, quién puede ver qué importa.
Por ejemplo, quizá quieras devolver notas internas solo a administradores, o conteos de comentarios solo en pantallas de detalle. Los Resources pueden manejar estas condiciones.
return [
'id' => (string) $this->id,
'title' => $this->title,
'internal_note' => $this->when(
$request->user()?->can('viewInternalNote', $this->resource),
$this->internal_note
),
];
Esto aclara el límite entre “presentación de API” y “permisos”.
Sin embargo, el punto clave es que la línea final de defensa debería ser Policy o lógica de authorize.
Los campos condicionales en Resources son control de visualización, no autorización en sí.
Mantener clara esta distinción mejora la seguridad.
8. DTO vs Resource: los Resources son “presentación externa”, los DTOs son “organización interna”
Una pregunta práctica común es cómo usar DTOs y Resources juntos.
Una distinción recomendada es esta.
- DTO: Organiza datos dentro de la aplicación
- Resource: Presenta datos externamente como respuesta HTTP
Por ejemplo, un flujo muy natural es agregar datos de múltiples modelos en un DTO y luego devolver ese DTO mediante un Resource.
8.1 Ejemplo de DTO
namespace App\Data;
class SalesSummaryData
{
public function __construct(
public readonly int $ordersCount,
public readonly int $customersCount,
public readonly int $salesTotal,
) {}
}
8.2 Lado del servicio
class DashboardSummaryService
{
public function getTodaySummary(): SalesSummaryData
{
return new SalesSummaryData(
ordersCount: Order::whereDate('created_at', today())->count(),
customersCount: User::whereDate('created_at', today())->count(),
salesTotal: Order::whereDate('created_at', today())->sum('total_amount'),
);
}
}
8.3 Lado del Resource
class SalesSummaryResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'orders_count' => $this->ordersCount,
'customers_count' => $this->customersCount,
'sales_total' => $this->salesTotal,
];
}
}
Esto separa el procesamiento interno de las respuestas externas y mejora la mantenibilidad.
9. Por qué no deberías devolver Eloquent directamente: protege información oculta y flexibilidad futura
Devolver modelos Eloquent directamente es fácil al principio. Pero a menudo causa los siguientes problemas.
- Las preocupaciones específicas de API se filtran demasiado en
hiddenovisible - Los cambios internos del modelo pueden afectar la API inesperadamente
- Listas, detalles, pantallas administrativas y apps móviles pueden necesitar formas diferentes
- La libertad futura para cambiar nombres de campos o estructuras queda limitada
Pasar los datos por un Resource facilita cambiar la estructura interna del modelo preservando el contrato de la API.
Este beneficio es menos visible en aplicaciones pequeñas, pero se vuelve extremadamente valioso en operación a largo plazo.
Una API es un contrato con consumidores, así que es más seguro no vincularla demasiado estrechamente a la estructura de Eloquent.
10. Diseño de paginación: estandariza una forma que el frontend pueda manejar fácilmente
La paginación de Laravel es cómoda, pero si no se entiende el significado de la respuesta, el frontend puede volverse difícil de gestionar.
En la práctica, ayuda estandarizar lo siguiente.
data: Registros realesmeta: Conteo total, página actual, última páginalinks: Enlaces siguiente / anterior- Condiciones de filtro mantenidas mediante query strings
Laravel Resource Collections ya proporcionan una estructura limpia, pero puedes añadir información complementaria con additional() cuando sea necesario.
return PostListResource::collection($posts)->additional([
'filters' => [
'q' => request('q'),
'status' => request('status'),
],
]);
Devolver “qué condiciones produjeron este resultado” estabiliza la gestión de estado del frontend.
Desde una perspectiva de accesibilidad, también facilita mostrar conteos de resultados y condiciones de filtro actuales en la pantalla.
11. Respuestas de error: unifica no solo las respuestas exitosas, sino también las formas de fallo
Una API está incompleta si solo las respuestas exitosas están limpias.
Los errores de validación, fallos de autenticación, fallos de autorización y respuestas de excepción también deberían ser consistentes. Esto facilita mucho la implementación frontend.
Por ejemplo, los errores de validación son fáciles de manejar con la siguiente forma.
{
"message": "Please check your input.",
"errors": {
"email": [
"The email address is required."
]
}
}
Laravel ya devuelve una estructura similar por defecto, pero decidir como equipo cuánto estandarizar aporta estabilidad.
También es útil unificar mensajes y códigos para errores de autorización y recursos no encontrados, para que el frontend pueda implementar una visualización de errores que no dependa solo del color.
En el lado de la UI, una respuesta bien organizada facilita implementar:
- Resúmenes de errores
- Conexiones de errores específicas por campo
- Notificaciones usando
role="alert"
En otras palabras, una UI accesible está profundamente conectada con el diseño de respuestas de error del backend.
12. Resources y rendimiento: úsalos pensando en prevenir N+1
Los Resources son cómodos, pero un uso incorrecto puede causar consultas innecesarias entre bastidores.
Un problema común es tocar relaciones de forma casual dentro de toArray().
Mal ejemplo:
'author_name' => $this->user->name,
Si esto se devuelve en una lista grande y user no está cargado, ocurre un problema N+1.
En la práctica, convierte los siguientes hábitos en estándar.
- Usar
with()en controladores o query builders - Usar
whenLoaded()en Resources - Separar las relaciones necesarias para listas y detalles
- Usar
withCount()para precargar conteos
Los Resources son herramientas de formateo de respuesta, no un reemplazo para la optimización de consultas.
Son potentes cuando se usan junto con un buen diseño de Eloquent.
13. Reglas de nomenclatura: los nombres de Resource son más claros cuando se basan en pantalla o propósito
Si los nombres de Resource son vagos, el mantenimiento se vuelve difícil.
Un buen enfoque es incluir el propósito en el nombre.
UserResource: Forma básicaUserListResource: Para listasUserDetailResource: Para detallesUserSummaryResource: Para resúmenes anidadosAdminUserResource: Para pantallas administrativas
Intentar manejar todo con un solo UserResource suele llevar a demasiadas condiciones.
Si las responsabilidades difieren entre lista, detalle y uso anidado, normalmente es mejor separarlas.
Esto es similar a nombrar componentes Blade: el mantenimiento es más fácil cuando el propósito está claro.
14. Diseña APIs fáciles de documentar: los Resources ayudan a hacer visibles las especificaciones
Introducir API Resources hace que las estructuras de respuesta sean claramente visibles en el código.
Esto ayuda directamente a la documentación y las revisiones de API.
Incluso en proyectos que aún no tienen OpenAPI completamente preparado, los desarrolladores pueden entender qué JSON devuelve una API mirando el Resource.
También facilita las conversaciones con desarrolladores frontend.
- La lista devuelve solo estos campos
- La respuesta de detalle incluye este objeto anidado
- El conteo está en
meta.total - Para errores, usa
errors.email[]
Cuando estos contratos son visibles, los cambios de especificación se vuelven más fáciles de discutir.
Los Resources reducen los costos de comunicación no solo para implementadores, sino para todo el equipo.
15. Pruebas: los Resources facilitan proteger la estructura de respuesta
Una vez introducidos los API Resources, se vuelve valioso proteger las estructuras de respuesta con pruebas.
Por ejemplo, una prueba de API de lista puede escribirse así.
public function test_posts_index_returns_expected_structure()
{
$user = User::factory()->create();
Post::factory()->count(3)->for($user)->create();
$response = $this->getJson('/api/posts');
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => [
'id',
'title',
'slug',
'excerpt',
'author_name',
'published_at',
],
],
'links',
'meta',
]);
}
Para APIs de detalle, también puedes verificar estructuras anidadas.
public function test_post_detail_returns_author()
{
$post = Post::factory()
->for(User::factory(), 'user')
->create();
$response = $this->getJson("/api/posts/{$post->id}");
$response->assertOk()
->assertJsonStructure([
'data' => [
'id',
'title',
'slug',
'body',
'published_at',
'author' => [
'id',
'name',
],
],
]);
}
Estas pruebas facilitan ver el impacto de los cambios en Resources.
Para APIs conectadas a aplicaciones frontend, bloquear las estructuras de respuesta con pruebas es especialmente valioso.
16. Errores comunes y cómo evitarlos
16.1 Construir arrays en cada controlador sin usar Resources
Esto se siente fácil al principio, pero los formatos de respuesta se dispersan.
Centralizarlos en Resources facilita la organización posterior.
16.2 Forzar listas y detalles dentro de un solo Resource
Si las condiciones se vuelven demasiado complejas, separa Resources de lista y detalle.
16.3 Devolver relaciones directamente y causar problemas N+1
Usa with(), withCount() y whenLoaded() juntos.
16.4 Poner demasiada lógica específica de API en accessors del modelo
El formateo de visualización y el formateo de API suelen ser diferentes.
Las formas de respuesta externas son más claras cuando se manejan con Resources.
16.5 Dejar las respuestas de error sin organizar
Las formas de respuesta de fallo importan tanto como las formas de respuesta exitosa.
Afectan directamente la implementación frontend y la calidad de accesibilidad.
17. Checklist para distribución
Diseño
- [ ] Los modelos Eloquent no se devuelven directamente
- [ ] El diseño de respuestas está centralizado en Resources
- [ ] Las responsabilidades de lista / detalle / anidado están separadas
- [ ] Los roles de DTO y Resource están claramente organizados
Rendimiento
- [ ] Las relaciones tocadas por Resources se basan en
with()/whenLoaded() - [ ] Los conteos usan
withCount() - [ ] Las listas devuelven solo las columnas necesarias
Calidad de respuesta
- [ ] La estructura de paginación está unificada
- [ ] Los campos condicionales están organizados
- [ ] Las formas de respuesta de error son consistentes
Pruebas
- [ ] Existen pruebas de estructura de API de lista
- [ ] Existen pruebas de estructura anidada de API de detalle
- [ ] Las estructuras JSON de error también se prueban
Integración frontend / accesibilidad
- [ ] Los estados y conteos son fáciles de mostrar en el frontend
- [ ] La estructura admite etiquetas que no dependen solo del color
- [ ] Los errores de entrada son fáciles de conectar a campos en JSON
18. Resumen
Laravel API Resources no son simplemente herramientas para formatear JSON.
Al colocar una “capa de presentación” entre modelos y respuestas, los contratos de API se vuelven más claros, y la integración frontend, el mantenimiento, las pruebas y los cambios futuros se vuelven mucho más fáciles.
Separar Resources de lista y detalle, formatear relaciones con Resources, usar DTOs para organización interna y estandarizar respuestas de error contribuyen a APIs que son legibles y difíciles de romper.
No necesitas hacer todo perfecto desde el principio. Empieza convirtiendo una API que actualmente devuelve un modelo directamente en un Resource. Ese primer paso hace que la diferencia de diseño sea fácil de sentir.

