Saltar al contenido principal

Qué son los API resources

Cuando construyes una API y devuelves modelos Eloquent tal cual como JSON, puedes filtrar mal columnas que querías ocultar o enviar al cliente una gran cantidad de datos que no necesita. Los API resources de Eloquent son una capa de transformación entre los modelos y la respuesta JSON. Con el método toArray() puedes definir explícitamente qué se incluye en la respuesta y con qué formato. Sus principales ventajas son:
  • Control total sobre los campos que se incluyen en la respuesta.
  • Un único lugar en el que renombrar campos y transformar valores.
  • Posibilidad de incluir campos condicionalmente.
  • Anidamiento de relaciones para mantener una estructura consistente.

Crear un resource

Genera una clase resource con el comando Artisan make:resource.
php artisan make:resource UserResource
La clase generada se ubica en el directorio app/Http/Resources.
<?php

namespace App\Http\Resources;

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

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}
Puedes acceder directamente a las propiedades del modelo con $this, porque la clase resource actúa como proxy hacia el modelo subyacente.

Uso en un controlador

Puedes devolver el resource definido desde un controlador o una ruta.
use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users/{user}', function (User $user) {
    return new UserResource($user);
});
También puedes usar el método toResource() del modelo.
Route::get('/users/{user}', function (User $user) {
    return $user->toResource();
});
toResource() localiza automáticamente la clase resource correspondiente al modelo (UserResource). Por defecto, la respuesta se envuelve en la clave data.
{
  "data": {
    "id": 1,
    "name": "Ana García",
    "email": "[email protected]",
    "created_at": "2024-01-15T10:00:00.000000Z",
    "updated_at": "2024-01-15T10:00:00.000000Z"
  }
}

Colecciones de resources

Para devolver varios modelos utiliza el método collection().
use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users', function () {
    return UserResource::collection(User::all());
});
O bien toResourceCollection() sobre una colección Eloquent.
return User::all()->toResourceCollection();

Resource collection personalizado

Si quieres añadir metadatos al conjunto de la colección, crea un resource collection propio.
php artisan make:resource UserCollection
<?php

namespace App\Http\Resources;

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

class UserCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => route('users.index'),
            ],
        ];
    }
}
Route::get('/users', function () {
    return new UserCollection(User::all());
});

Transformar y renombrar campos

Puedes renombrar campos y transformar valores dentro de toArray().
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'full_name' => $this->name,                         // Renombrar campo
        'email_address' => $this->email,                    // Renombrar campo
        'role' => strtoupper($this->role),                  // Transformar valor
        'registered_at' => $this->created_at->toDateString(), // Formatear fecha
    ];
}

Campos condicionales

when(): añadir un campo si se cumple una condición

Cuando solo quieras incluir un campo si se cumple una condición, utiliza when().
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        // Se incluye solo si el usuario autenticado es administrador
        'secret_token' => $this->when(
            $request->user()?->isAdmin(),
            $this->secret_token
        ),
    ];
}
Si la condición de when() es false, la clave se elimina por completo de la respuesta.

mergeWhen(): añadir varios campos con la misma condición

Para incluir varios campos bajo la misma condición, utiliza mergeWhen().
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        $this->mergeWhen($request->user()?->isAdmin(), [
            'admin_note' => $this->admin_note,
            'internal_id' => $this->internal_id,
        ]),
    ];
}

whenLoaded(): incluir la relación solo si está cargada

Al incluir una relación solo cuando se haya cargado con eager loading, evitas el problema N+1 sin sacrificar la flexibilidad de la respuesta.
use App\Http\Resources\PostResource;

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        // Solo se incluye si posts se ha cargado con with()
        'posts' => PostResource::collection($this->whenLoaded('posts')),
    ];
}
Puedes decidir en el controlador si cargar o no la relación.
// Con los posts incluidos
return new UserResource($user->load('posts'));

// Sin los posts
return new UserResource($user);

whenCounted(): incluir un conteo si se ha cargado

Incluye el conteo obtenido con loadCount() solo si está disponible.
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'posts_count' => $this->whenCounted('posts'),
    ];
}
return new UserResource($user->loadCount('posts'));

Resources anidados

Puedes anidar relaciones a través de otra clase resource para mantener una estructura consistente.
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'author' => new UserResource($this->whenLoaded('user')),
            'comments' => CommentResource::collection($this->whenLoaded('comments')),
            'published_at' => $this->published_at?->toDateString(),
        ];
    }
}
// Controlador
$post = Post::with(['user', 'comments'])->findOrFail($id);

return new PostResource($post);
{
  "data": {
    "id": 1,
    "title": "Aprende API resources con Laravel",
    "author": {
      "id": 5,
      "name": "Ana García",
      "email": "[email protected]"
    },
    "comments": [
      { "id": 10, "body": "¡Muy útil!" }
    ]
  }
}

Añadir metadatos

with(): metadatos de nivel superior

Para añadir metadatos al conjunto de la colección, sobrescribe el método with().
class UserCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }

    public function with(Request $request): array
    {
        return [
            'meta' => [
                'version' => '1.0',
                'generated_at' => now()->toIso8601String(),
            ],
        ];
    }
}
Ejemplo de respuesta:
{
  "data": [...],
  "meta": {
    "version": "1.0",
    "generated_at": "2024-01-15T10:00:00+00:00"
  }
}

additional(): metadatos dinámicos

Para añadir metadatos de forma dinámica desde el controlador utiliza additional().
return User::all()
    ->load('roles')
    ->toResourceCollection()
    ->additional(['meta' => [
        'total_admins' => User::where('role', 'admin')->count(),
    ]]);

Integración con la paginación

Basta con pasar el resultado paginado al resource para que se añadan automáticamente meta y links.
Route::get('/users', function () {
    return UserResource::collection(User::paginate(15));
});
O bien:
return User::paginate(15)->toResourceCollection();
Ejemplo de respuesta:
{
  "data": [
    { "id": 1, "name": "Ana García" },
    { "id": 2, "name": "Luis Pérez" }
  ],
  "links": {
    "first": "https://example.com/users?page=1",
    "last": "https://example.com/users?page=5",
    "prev": null,
    "next": "https://example.com/users?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 5,
    "per_page": 15,
    "to": 15,
    "total": 72
  }
}
En las respuestas paginadas la clave data se conserva incluso aunque hayas llamado a withoutWrapping(), porque debe coexistir con las claves meta y links de la paginación.

Desactivar el wrapping de datos

Por defecto el resource más externo se envuelve en la clave data. Para desactivarlo, invoca withoutWrapping() en el método boot() de AppServiceProvider.
use Illuminate\Http\Resources\Json\JsonResource;

public function boot(): void
{
    JsonResource::withoutWrapping();
}
withoutWrapping() afecta únicamente al wrapping más externo. Las claves data que tú mismo hayas definido no se eliminan.

Ejemplo práctico: implementación de una API de usuarios

Diseño de respuestas coherentes con resources en una API de gestión de usuarios.

UserResource

<?php

namespace App\Http\Resources;

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

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'avatar_url' => $this->avatar_url,
            'role' => $this->role,
            // Solo visible para administradores
            'created_at' => $this->when(
                $request->user()?->isAdmin(),
                $this->created_at->toDateString()
            ),
            // Se incluyen solo si están cargados
            'posts' => PostResource::collection($this->whenLoaded('posts')),
            'posts_count' => $this->whenCounted('posts'),
        ];
    }
}

UserController

<?php

namespace App\Http\Controllers\Api;

use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class UserController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        $users = User::withCount('posts')->paginate(20);

        return UserResource::collection($users);
    }

    public function show(User $user): UserResource
    {
        $user->loadCount('posts')->load('posts');

        return new UserResource($user);
    }
}

Páginas relacionadas

Introducción a las relaciones de Eloquent

Revisa cómo definir relaciones y aplicar eager loading.

Paginación

Consulta cómo combinar los resultados paginados con los API resources.
Última modificación el 13 de julio de 2026