API Resources: transformando modelos em JSON

Introdução aos API Resources

Ao construir APIs RESTful em PHP, um desafio constante é transformar modelos (Eloquent Models, Doctrine Entities, etc.) em JSON de forma consistente e controlada. Os API Resources surgem como uma camada de transformação que separa a lógica de apresentação dos dados da lógica de negócio.

No ecossistema Laravel — framework PHP mais popular — os API Resources foram introduzidos no Laravel 5.5 como uma alternativa nativa ao antigo pacote Fractal. Eles permitem que você defina exatamente como cada modelo será serializado, evitando expor atributos sensíveis e garantindo formato padronizado.

Existem dois conceitos fundamentais:
- Resource: representa um único modelo (ex: UserResource)
- ResourceCollection: representa uma coleção de modelos (ex: UserCollection)

Os cenários ideais incluem qualquer API RESTful, especialmente quando você precisa:
- Ocultar campos sensíveis (senhas, tokens)
- Renomear chaves (snake_case para camelCase)
- Incluir relacionamentos condicionalmente
- Versionar sua API

Estrutura Básica de um Resource

Vamos criar nosso primeiro Resource usando o comando Artisan:

php artisan make:resource UserResource

Isso gera a classe app/Http/Resources/UserResource.php. O método principal é toArray():

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at->format('Y-m-d H:i:s'),
            'updated_at' => $this->updated_at->format('Y-m-d H:i:s'),
        ];
    }
}

Para usar no controller:

use App\Http\Resources\UserResource;

public function show($id)
{
    $user = User::findOrFail($id);
    return new UserResource($user);
}

A resposta será algo como:

{
    "data": {
        "id": 1,
        "name": "João Silva",
        "email": "joao@exemplo.com",
        "created_at": "2024-01-15 10:30:00",
        "updated_at": "2024-01-15 10:30:00"
    }
}

Personalizando a Resposta JSON

Renomeando chaves e formatando dados

public function toArray($request)
{
    return [
        'user_id' => $this->id,
        'full_name' => $this->name,
        'email_address' => $this->email,
        'member_since' => $this->created_at->diffForHumans(),
        'is_admin' => $this->is_admin ? true : false,
    ];
}

Incluindo relacionamentos com whenLoaded

Para evitar N+1 queries, use whenLoaded():

public function toArray($request)
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        'body' => $this->body,
        'author' => new UserResource($this->whenLoaded('author')),
        'comments_count' => $this->whenLoaded('comments', function () {
            return $this->comments->count();
        }),
    ];
}

Condicionais com when(), mergeWhen() e unless()

public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        $this->mergeWhen($this->isAdmin(), [
            'secret_token' => $this->api_token,
            'permissions' => $this->getAllPermissions(),
        ]),
        'avatar' => $this->when($this->hasAvatar(), $this->avatar_url),
    ];
}

Trabalhando com Coleções

Para listas de modelos, crie uma ResourceCollection:

php artisan make:resource PostCollection
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class PostCollection extends ResourceCollection
{
    public $collects = PostResource::class;

    public function toArray($request)
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => url('/api/posts'),
            ],
        ];
    }

    public function paginationInformation($request, $paginated, $default)
    {
        return [
            'meta' => [
                'current_page' => $paginated['current_page'],
                'total' => $paginated['total'],
                'per_page' => $paginated['per_page'],
                'last_page' => $paginated['last_page'],
            ],
        ];
    }
}

No controller:

public function index()
{
    $posts = Post::with('author')->paginate(15);
    return new PostCollection($posts);
}

Aninhamento e Relacionamentos Profundos

Resources podem ser aninhados naturalmente:

class PostResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'author' => new UserResource($this->whenLoaded('author')),
            'comments' => CommentResource::collection($this->whenLoaded('comments')),
        ];
    }
}

Dica de performance: Sempre carrege relacionamentos antecipadamente:

$post = Post::with(['author', 'comments.user'])->find($id);
return new PostResource($post);

Transformações Avançadas

Resource::collection() vs new ResourceCollection()

// Método 1 - usando collection estático
return UserResource::collection(User::all());

// Método 2 - instanciando ResourceCollection
return new UserCollection(User::paginate(15));

O primeiro método é mais simples para coleções sem paginação. O segundo oferece mais controle sobre metadados.

Adicionando metadados extras

Sobrescreva o método with():

class UserResource extends JsonResource
{
    public function with($request)
    {
        return [
            'meta' => [
                'api_version' => '1.0',
                'timestamp' => now()->toIso8601String(),
            ],
        ];
    }
}

Ou use additional() no controller:

return (new UserResource($user))
    ->additional(['meta' => ['request_id' => request()->header('X-Request-ID')]]);

Boas Práticas e Padrões

Versionamento de API

Crie diretórios separados:

app/Http/Resources/
├── v1/
│   ├── UserResource.php
│   └── PostResource.php
└── v2/
    ├── UserResource.php
    └── PostResource.php

Reutilização com Traits

trait TimestampResource
{
    public function timestamps()
    {
        return [
            'created_at' => $this->created_at->toIso8601String(),
            'updated_at' => $this->updated_at->toIso8601String(),
        ];
    }
}

class UserResource extends JsonResource
{
    use TimestampResource;

    public function toArray($request)
    {
        return array_merge([
            'id' => $this->id,
            'name' => $this->name,
        ], $this->timestamps());
    }
}

Testando Resources

public function test_user_resource_formats_correctly()
{
    $user = User::factory()->create(['name' => 'Teste']);

    $resource = (new UserResource($user))->response()->getData(true);

    $this->assertEquals('Teste', $resource['data']['name']);
    $this->assertArrayNotHasKey('password', $resource['data']);
}

Comparação com Alternativas

Característica API Resources (Laravel) Fractal Serializers Manuais
Integração nativa ✅ Sim ❌ Pacote externo ❌ Manual
Performance ⚡ Excelente ⚡ Boa ⚡ Variável
Facilidade de manutenção ✅ Alta ✅ Alta ❌ Média
Suporte a JSON:API ❌ Não nativo ✅ Sim ✅ Manual

Para APIs complexas que seguem especificação JSON:API, considere o pacote Spatie/laravel-json-api-paginate ou Spatie/data-transfer-object. Para projetos menores, API Resources continuam sendo a melhor escolha.


Os API Resources transformam a forma como serializamos modelos em JSON, oferecendo controle granular, performance e facilidade de manutenção. Ao dominar conceitos como whenLoaded, aninhamento e coleções, você constrói APIs robustas e elegantes em PHP.

Comece hoje mesmo a refatorar seus controllers e descubra como essa camada de transformação pode simplificar sua vida como desenvolvedor backend.

Referências