Módulo 5 · Lección 2 20 min

API Resources — formatear respuestas JSON

Domina los API Resources de Laravel para controlar exactamente qué datos devuelve tu API, transformar respuestas y crear capas de presentación limpias.

¿Por qué necesitas API Resources?

Cuando devuelves un modelo Eloquent directamente en tu controlador con response()->json($producto), Laravel serializa el modelo tal cual, incluyendo todos sus campos, incluso los que no deberían ser públicos, como password, remember_token o campos internos de la base de datos.

Los API Resources resuelven este problema actuando como una capa de transformación entre tu modelo y la respuesta JSON. Con ellos puedes:

  • Elegir exactamente qué campos exponer.
  • Renombrar campos para adaptarlos a la convención camelCase del frontend.
  • Añadir campos calculados o relaciones anidadas.
  • Incluir metadatos en la respuesta (paginación, totales, etc.).
  • Mantener una estructura de respuesta consistente en toda la API.

Crear un API Resource

php artisan make:resource ProductoResource

Esto crea el archivo app/Http/Resources/ProductoResource.php:

// app/Http/Resources/ProductoResource.php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProductoResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'          => $this->id,
            'nombre'      => $this->nombre,
            'descripcion' => $this->descripcion,
            'precio'      => (float) $this->precio,
            'stock'       => $this->stock,
            'disponible'  => $this->stock > 0,
            'creadoEn'    => $this->created_at->format('d/m/Y H:i'),
        ];
    }
}

Fíjate en lo que hemos hecho:

  • Excluimos campos sensibles como updated_at.
  • Forzamos que precio sea un float (evitando que llegue como string desde MySQL).
  • Añadimos un campo calculado disponible basado en el stock.
  • Renombramos created_at a creadoEn en formato legible.

Usar el Resource en el controlador

// app/Http/Controllers/Api/ProductoController.php

use App\Http\Resources\ProductoResource;

public function show(Producto $producto): ProductoResource
{
    return new ProductoResource($producto);
}

public function store(Request $request): ProductoResource
{
    $validated = $request->validate([
        'nombre'  => 'required|string|max:255',
        'precio'  => 'required|numeric|min:0',
        'stock'   => 'required|integer|min:0',
    ]);

    $producto = Producto::create($validated);

    return new ProductoResource($producto);
}

La respuesta JSON resultante tendrá este formato:

{
  "data": {
    "id": 1,
    "nombre": "Teclado mecánico",
    "descripcion": "Teclado TKL con switches Cherry MX Red",
    "precio": 89.99,
    "stock": 50,
    "disponible": true,
    "creadoEn": "16/04/2026 10:30"
  }
}

Nota que Laravel envuelve automáticamente la respuesta en una clave data. Esto es parte de la especificación JSON:API y se puede desactivar, pero es una buena práctica mantenerlo.

Colecciones de recursos

Para devolver múltiples recursos, puedes usar ProductoResource::collection():

public function index(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection
{
    $productos = Producto::paginate(15);
    return ProductoResource::collection($productos);
}

Cuando se usa con paginación, la respuesta incluye automáticamente los metadatos de paginación:

{
  "data": [
    { "id": 1, "nombre": "Teclado mecánico", ... },
    { "id": 2, "nombre": "Ratón inalámbrico", ... }
  ],
  "links": {
    "first": "http://localhost:8000/api/productos?page=1",
    "last": "http://localhost:8000/api/productos?page=4",
    "prev": null,
    "next": "http://localhost:8000/api/productos?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 4,
    "per_page": 15,
    "to": 15,
    "total": 58
  }
}

Resource Collections personalizadas

Si necesitas añadir metadatos propios a la colección, crea una clase de colección dedicada:

php artisan make:resource ProductoCollection --collection
// app/Http/Resources/ProductoCollection.php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class ProductoCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total'      => $this->collection->count(),
                'enStock'    => $this->collection->where('stock', '>', 0)->count(),
                'agotados'   => $this->collection->where('stock', 0)->count(),
            ],
        ];
    }
}

Úsala así en el controlador:

public function index(): ProductoCollection
{
    return new ProductoCollection(Producto::all());
}

Incluir relaciones condicionales

Una de las funcionalidades más útiles de los API Resources es poder incluir relaciones de forma condicional, solo cuando han sido cargadas con Eager Loading:

// app/Http/Resources/ProductoResource.php

public function toArray(Request $request): array
{
    return [
        'id'       => $this->id,
        'nombre'   => $this->nombre,
        'precio'   => (float) $this->precio,
        'categoria' => new CategoriaResource($this->whenLoaded('categoria')),
        'reviews'   => ReviewResource::collection($this->whenLoaded('reviews')),
    ];
}

En el controlador, cargas la relación explícitamente cuando la necesitas:

// Sin relación (respuesta ligera)
public function index()
{
    return ProductoResource::collection(Producto::paginate(15));
}

// Con relación (respuesta completa)
public function show(Producto $producto)
{
    return new ProductoResource($producto->load('categoria', 'reviews'));
}

Esto evita el problema N+1 y permite controlar cuándo se incluyen datos adicionales.

Añadir metadatos con with()

Puedes añadir datos extra a cualquier respuesta de Resource sobreescribiendo el método with():

// app/Http/Resources/ProductoResource.php

public function with(Request $request): array
{
    return [
        'meta' => [
            'version' => 'v1',
            'autor'   => 'Mi Tienda API',
        ],
    ];
}

La respuesta resultante incluirá:

{
  "data": { ... },
  "meta": {
    "version": "v1",
    "autor": "Mi Tienda API"
  }
}

Respuestas condicionales con when()

El método when() permite incluir un campo solo si se cumple una condición:

public function toArray(Request $request): array
{
    return [
        'id'        => $this->id,
        'nombre'    => $this->nombre,
        'precio'    => (float) $this->precio,
        // Solo incluir el costo si el usuario es administrador
        'costo'     => $this->when($request->user()?->esAdmin(), $this->costo),
        // Solo incluir el token si acaba de ser creado
        'api_token' => $this->when($this->wasRecentlyCreated, $this->api_token),
    ];
}

Estructura de respuesta consistente

Una buena práctica es crear un Resource base que todos tus recursos extiendan, garantizando una estructura uniforme:

// app/Http/Resources/BaseResource.php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class BaseResource extends JsonResource
{
    public function with($request): array
    {
        return [
            'success' => true,
            'version' => config('app.api_version', 'v1'),
        ];
    }
}

Los API Resources de Laravel son una herramienta poderosa que separa la lógica de presentación de la lógica de negocio. Usarlos desde el principio en tus proyectos hará que tus APIs sean más mantenibles, seguras y consistentes.

Quiz

Pon a prueba lo aprendido

Responde las preguntas para comprobar que has entendido los conceptos clave.

1. ¿Para qué sirven los API Resources en Laravel?

2. ¿Qué comando de Artisan crea un API Resource para el modelo Producto?

3. ¿Qué clase se usa para devolver una colección de recursos con metadatos adicionales?