Ir al contenido principal
Laravel, shipping fast.

Un Resource es una clase que convierte un objeto en el JSON que ven tus consumidores. En la mayoría de las aplicaciones de Laravel ese objeto es un modelo de Eloquent. Aquí es un objeto de licencia tipado que volvió del proveedor, y el Resource funciona igual. (Documentación de Laravel: Eloquent: API Resources.)

php artisan make:resource LicenseResource
// app/Http/Resources/LicenseResource.php
/** @property License $resource */
class LicenseResource extends JsonResource
{
    /**
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'key' => $this->resource->key,
            'name' => $this->resource->name,
            'domains' => $this->resource->domains,
            'created_at' => $this->resource->createdAt,
        ];
    }
}

Un Resource es una lista blanca explícita. Nada sale de la API si no está nombrado en toArray().

Eso importa más de lo que parece. El objeto que hay detrás de un Resource tiende a crecer: notas internas, un costo, un secreto del proveedor. Sin un Resource, estás a un return $license de publicarlo todo. Con uno, un campo interno nuevo sigue siendo interno hasta que alguien añade aquí una línea a propósito, y esa línea aparece en la revisión de código.

Los Resources también te dan el sobre gratis. Devuelve uno desde un controlador y Laravel lo envuelve:

{
    "data": {
        "key": "lic_789",
        "name": "New License",
        "domains": ["example.com"],
        "created_at": "2026-02-12T10:30:00Z"
    }
}

Devuelve una colección y obtienes la misma clave alrededor de una lista:

public function index(): AnonymousResourceCollection
{
    return LicenseResource::collection(Licenses::all());
}
{
    "data": [
        {
            "key": "lic_123",
            "name": "My License",
            "domains": ["example.com"],
            "created_at": "2026-02-12T10:30:00Z"
        }
    ]
}

Una sola clave, data, para un elemento o para muchos. Un consumidor escribe un único parser y le sirve en todos los endpoints. Y cuando una colección viene de una consulta paginada de Eloquent, Laravel añade links y meta junto a data sin que escribas una línea. Las licencias nunca se paginan, porque el proveedor devuelve la lista completa, pero los consumidores del Capítulo 5 sí, y el formato ya está decidido.

Si tus consumidores esperan la especificación JSON:API, Laravel 13 también trae clases de Resource para eso. El resto de este libro usa el formato simple de arriba. (Documentación de Laravel: Eloquent: API Resources › JSON:API Resources.)

Códigos de estado

Un Resource devuelto desde un controlador responde 200. Para un modelo de Eloquent recién creado, Laravel responde 201 por su cuenta. Nuestra licencia no es un modelo, así que store lo dice de forma explícita:

return LicenseResource::make($license)
    ->response()
    ->setStatusCode(Response::HTTP_CREATED);

No todas las respuestas llevan un Resource. Borrar una licencia significa pedirle al proveedor que la borre, lo que puede ser lento, así que el endpoint encola el trabajo y responde de inmediato:

// app/Http/Controllers/LicenseController.php
public function destroy(string $license): JsonResponse
{
    DeleteLicense::dispatch($license);

    return response()->json(
        ['message' => __('licenses.delete_queued')],
        Response::HTTP_ACCEPTED,
    );
}

$license es el segmento {license} de la URL, que es la clave de la licencia. Un 202 dice «lo recibí y se va a procesar». Un 200 diría «ya está hecho», y eso sería mentira. El Capítulo 10 construye DeleteLicense y explica qué tiene que significar «se va a procesar» antes de que tengas derecho a prometerlo.

Por qué no una clase de respuesta propia

Verás proyectos de Laravel que envuelven cada respuesta en una clase propia: un SuccessResponse, un ApiResponse, una clase base dueña del sobre. Yo las he construido, y el servicio del que sale este libro todavía carga con un juego de ellas.

Rara vez valen la pena. Los Resources ya son dueños del sobre. response()->json() ya es dueño del estado y las cabeceras. Los middleware ya son dueños de cualquier cosa que deba añadirse a todas las respuestas. Una clase envoltorio añade un cuarto lugar donde se decide la forma de una respuesta, y el día que no coincida con los otros tres tendrás la inconsistencia que intentabas evitar.

Usa lo que Laravel te da hasta que puedas nombrar aquello que no puede hacer. Entonces añade exactamente eso.

El controlador entero

// app/Http/Controllers/LicenseController.php
class LicenseController
{
    public function index(): AnonymousResourceCollection
    {
        return LicenseResource::collection(Licenses::all());
    }

    public function store(
        StoreLicenseRequest $request,
    ): JsonResponse {
        $license = Licenses::create(
            name: $request->validated('name'),
            domain: $request->validated('domain'),
        );

        return LicenseResource::make($license)
            ->response()
            ->setStatusCode(Response::HTTP_CREATED);
    }

    // ... destroy() como se mostró arriba
}

Esa es toda la capa HTTP de las licencias. Ni URL del proveedor, ni token, ni claves de array, ni un if. Un desarrollador que lee index entiende store.

Como las formas son las del propio Laravel, los tests también son cortos:

// tests/Feature/LicenseTest.php
it('rejects a license without a name', function () {
    $this->postJson('/api/licenses', ['domain' => 'a.com'])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['name']);
});

assertJsonValidationErrors conoce el formato de error por defecto. Funciona solo porque la API conservó ese formato.

Resumen del capítulo 2

  • Un controlador recibe una petición y devuelve una respuesta. Nada más.
  • La validación vive en un FormRequest. Los mensajes viven en los archivos de idioma.
  • Conserva el formato de error de validación de Laravel, y fuerza JSON para /api una sola vez, en bootstrap/app.php.
  • Un Resource es la lista blanca de lo que sale de la API, y te da el sobre data para un elemento o para muchos.
  • Los códigos de estado son decisión del controlador: 201 para lo creado, 202 para lo aceptado y encolado.
  • No construyas una capa de respuestas propia hasta que puedas nombrar lo que la de Laravel no puede hacer.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.