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
- Documentação Oficial Laravel - API Resources — Guia completo sobre criação e uso de API Resources no Laravel.
- Laravel News - Eloquent API Resources Tutorial — Tutorial prático com exemplos avançados de transformação de modelos.
- Laracasts - API Resources Series — Série em vídeo ensinando desde conceitos básicos até técnicas avançadas.
- GitHub - Laravel Framework (JsonResource) — Código-fonte oficial da classe JsonResource para consulta.
- Spatie - Laravel Data Package — Alternativa moderna para transformação de dados, ideal para APIs que exigem DTOs.
- Laravel Daily - API Resources Tips — Dicas rápidas e truques para otimizar o uso de Resources.
- Stack Overflow - When to use API Resources — Discussão sobre cenários ideais e boas práticas no uso de Resources.