JSON:API en Laravel 13: crea APIs estándar con JsonApiResource sin paquetes
Laravel 13 responde JSON:API de fábrica: con make:resource --json-api generas recursos con type, id, attributes y relationships según jsonapi.org, sin instalar paquetes. Si cada endpoint de tu API devuelve un JSON distinto, aquí tienes el estándar que estabiliza el contrato con tu frontend.
Qué es JSON:API y por qué estandariza tus respuestas
JSON:API no es un framework ni una librería: es una especificación abierta mantenida por jsonapi.org que define cómo debe verse una respuesta JSON en una API REST. Cuando tu API la cumple, cualquier cliente que conozca la spec sabe exactamente dónde encontrar los datos, sin depender de convenciones internas de tu equipo.
La especificación jsonapi.org: type, id, attributes y relationships
La regla central es que cada recurso se serializa con una estructura fija: type e id lo identifican de forma única, los datos propios van en attributes y las referencias a otros recursos en relationships. Esa separación entre lo que el recurso es y a qué está conectado es lo que permite pedir relaciones anidadas o campos parciales sin romper el contrato.
El problema que resuelve: un JSON distinto en cada endpoint
Cualquiera que haya mantenido una API REST unos años conoce el escenario: el endpoint de usuarios devuelve { "user": { ... } }, el de posts devuelve la lista directamente y los nombres de campos cambian según quién escribió cada controlador. El frontend termina lleno de casos especiales. JSON:API elimina esa ambigüedad: la forma de la respuesta deja de ser una decisión de cada endpoint y pasa a ser una regla del sistema.
La cabecera Content-Type: application/vnd.api+json
Parte de cumplir la spec es declarar el formato con la cabecera Content-Type: application/vnd.api+json, tanto en las respuestas como en las peticiones que envían cuerpo. Los clientes JSON:API la usan para detectar el formato y aplicar sus reglas de parseo.
Laravel 13 trae JSON:API de primera mano
Hasta Laravel 12, cumplir la spec exigía instalar un paquete y aprender su forma particular de hacer las cosas. Con Laravel 13 ese salto desaparece: el soporte llega integrado en el núcleo.
Antes: paquetes de terceros para cumplir la spec
Las opciones habituales eran paquetes como Laravel JSON:API o spatie/laravel-json-api. Funcionaban bien, pero añadían una dependencia más, un DSL propio que memorizar y una capa de abstracción que a veces complicaba el control fino de la salida. Tapaban un hueco que el framework no cubría.
Ahora: JsonApiResource nativo con make:resource --json-api
Laravel 13 incorpora las clases de recurso JSON:API de primera mano. El generador artisan entiende el flag --json-api:
php artisan make:resource PostResource --json-apiEl comando crea una clase que extiende JsonApiResource en lugar del clásico JsonResource, y el framework se encarga de serializar attributes, relationships, links de paginación y sparse fieldsets siguiendo la spec. El resultado: tu API puede ser estándar sin una sola dependencia nueva.
Crear tu primer JsonApiResource
El punto de partida de cualquier endpoint JSON:API en Laravel 13 es un recurso por modelo. Vamos a crear el de los posts de un blog.
El comando artisan y la estructura del archivo generado
El archivo aparece en app/Http/Resources y su esqueleto es intencionadamente pequeño: tú declaras qué atributos expones y qué relaciones existen, y Laravel decide cómo serializarlo. Esa separación mantiene el código limpio cuando el modelo crece.
Definir attributes con el método attributes()
Los atributos que verá el cliente se declaran en attributes():
public function attributes(Request $request): array
{
return [
'title' => $this->title,
'slug' => $this->slug,
'body' => $this->body,
'published_at' => $this->published_at,
];
}Decides explícitamente qué campos salen: fechas internas, claves foráneas o columnas técnicas se quedan fuera con solo no listarlas. Si quieres ocultar un campo, lo quitas de aquí y el cambio se propaga a todos los endpoints que usan el recurso.
Resource type e id: qué identifica a tu recurso
Cada recurso declara un type (por ejemplo posts) que lo identifica frente a otros tipos, y el id se corresponde con la clave primaria del modelo. Los clientes usan ese par type+id para cachear recursos y resolver las referencias de las relaciones, así que conviene que el type sea estable desde el primer día: cambiarlo después obliga a actualizar todo el frontend.
Relaciones: includes sin N+1
Una de las grandes ventajas de JSON:API es que el cliente decide cuándo viajan las relaciones en la misma respuesta, mediante el parámetro include. En Laravel 13 eso se traduce en declarar las relaciones en el recurso.
Definir relationships en el recurso
En relationships() indicas qué relaciones puede incluir el cliente y cómo serializarlas:
public function relationships(Request $request): array
{
return [
'author' => fn () => new UserResource($this->author),
'comments' => fn () => CommentResource::collection($this->comments),
];
}Las closures se evalúan solo si el cliente pide la relación, de modo que una petición sencilla no paga el coste de serializar todo el grafo de objetos.
Cargar relaciones con ?include=author
Con esa declaración, una petición como ?include=author devuelve el post con su autor embebido en el mismo documento, y ?include=author.comments funciona para relaciones anidadas. El resultado es un compound document: una sola respuesta, sin peticiones extra en cascada.
Combinar includes con eager loading en el controlador
Que el cliente pida la relación no significa que Eloquent la cargue sola: el controlador sigue siendo responsable de evitar el problema N+1. El patrón es idéntico al de siempre, como explico en mi artículo sobre cómo eliminar el N+1 en Eloquent, solo que aquí la lista de relaciones a precargar puede venir del propio include de la petición.
Sparse fieldsets: que el cliente pida solo lo que usa
La spec permite que el cliente limite los atributos que recibe por tipo de recurso. Es la herramienta perfecta para listados donde solo se muestran dos o tres campos.
El parámetro ?fields[posts]=title,body
Con una petición como ?fields[posts]=title,body, la API responde únicamente esos dos atributos para cada post, ignorando el resto aunque estén declarados en attributes(). Como el filtro se aplica por tipo, puedes pedir campos distintos para posts, autores y comentarios en la misma llamada.
Menos ancho de banda y respuestas más rápidas
En una app móvil o en un dashboard que lista cientos de filas, recortar payloads pesados marca una diferencia real en tiempo de carga y consumo de datos. Los sparse fieldsets convierten esa optimización en algo que decide el cliente, no el backend, lo que simplifica el trabajo cuando una misma API alimenta a una web de escritorio y a una app móvil.
Links y meta de paginación
Las colecciones JSON:API no devuelven solo una lista: incluyen la información de paginación de forma estándar, para que el cliente pueda navegar por las páginas sin convenciones propias.
Paginación con links first, last, next y prev
Si el controlador pagina la consulta con paginate(), el recurso de colección añade los links first, last, next y prev automáticamente. El frontend los consume directamente, sin parsear cabeceras ni adivinar el formato de la URL de la página siguiente.
Meta información del recurso
Junto a los links, la respuesta incluye meta con datos útiles de la paginación, como el total de elementos o la página actual. Si tu API necesita exponer otra información global, la spec deja espacio para ampliar ese bloque meta de forma controlada.
JsonResource clásico vs JsonApiResource
Lo primero: el JsonResource de toda la vida no desaparece en Laravel 13. Sigue generándose con make:resource sin flag y sigue siendo la opción correcta en muchos casos. La pregunta no es cuál es mejor, sino cuándo usar cada uno.
Cuándo seguir usando JsonResource
El recurso clásico te da control total: wrapping de datos, atributos condicionales con when(), relaciones condicionales con whenLoaded() y la libertad de devolver el JSON exacto que necesites. Para APIs internas de una sola aplicación, respuestas ad hoc o endpoints legacy que no puedes tocar, sigue siendo la herramienta más directa.
Migrar una API existente endpoint a endpoint
Si ya tienes una API con JsonResource, no necesitas reescribirla en un fin de semana. La migración se hace endpoint a endpoint: creas el JsonApiResource de un modelo, cambias la respuesta de ese endpoint, corres tus tests y repites. El resto de la API sigue funcionando con su formato anterior, y cada endpoint migrado reduce la deuda de consistencia.
Spatie Laravel Query Builder como compañero para filtros y orden
JSON:API estandariza la salida, pero no parsea los parámetros de entrada como filtros u orden. La documentación oficial de Laravel recomienda acompañarlo con Spatie Laravel Query Builder para traducir esos parámetros a consultas Eloquent; si trabajas con consultas más complejas, te interesa también mi guía de Query Builder avanzado en Laravel 13.
Caso real: API de posts con autor y comentarios
Vamos a juntarlo todo con el ejemplo clásico: una API Laravel 13 protegida con Sanctum para un blog, que pasa de respuestas inconsistentes a JSON:API estándar.
Endpoints GET /api/posts y GET /api/posts/{post}
Defines los recursos (PostResource, UserResource y CommentResource con --json-api) y en el controlador precargas las relaciones que el cliente puede pedir:
Route::get('/api/posts', function () {
return PostResource::collection(
Post::query()->with(['author', 'comments'])->paginate()
);
});La protección con Sanctum se mantiene igual que siempre; lo único que cambia es la capa de serialización. Si te falta contexto sobre cómo endurecer esos endpoints, el artículo de rate limiting para APIs en Laravel 13 es un buen complemento.
Respuesta final verificada con curl
Una petición que pide relaciones y campos parciales se ve así:
curl -H "Accept: application/vnd.api+json" \
"https://api.example.com/api/posts?include=author&fields[posts]=title,body"Y la respuesta sigue la estructura de la spec: cada post con su type, su id, los atributos filtrados y la relación con el autor resuelta en el mismo documento. Ese es el contrato estable que tu frontend de React, Vue o móvil puede consumir sin casos especiales.
Conclusión
JSON:API nativo en Laravel 13 elimina la excusa de que cumplir la spec exige demasiada infraestructura: con un flag en artisan, un método para atributos y otro para relaciones, tu API responde en un formato estándar que cualquier cliente entiende. Empieza por un solo endpoint, míralo funcionar con sparse fieldsets e includes, y deja que la consistencia se extienda al resto. Si te ha sido útil, sigue leyendo el blog para más guías de Laravel 13, APIs y desarrollo web.