Recursos de API Laravel e Desenvolvimento de API RESTful
Recursos de API é o "empacotador de dados" do Laravel—ele controla quais campos são expostos aos clientes, como são formatados e como os relacionamentos são aninhados, garantindo que a saída da API siga um padrão consistente.
1. O Que Você Vai Aprender
- Classe Resource e ResourceCollection: Conversão e Formatação de Dados
- Recursos associados aninhados: Carregamento condicional de recursos associados via
whenLoaded() - Encapsulamento de Paginação, Filtragem e Ordenação para Recursos de API
- Especificações de Design de API RESTful: URIs, Verbos, Códigos de Status e Formatos de Erro
- Controle de Versão de API: Grupos de Rotas v1/v2 e Herança de Recursos
2. Uma História Real de um Desenvolvedor Front-End
(1) Dor: O formato JSON retornado pela API causa falha no front-end
Alice encontrou um pesadelo ao integrar com a API do ShopMetrics—/shops retornava {shops: [...]}, mas /orders retornava {data: [...]}, e /products simplesmente retornava um array. Os nomes dos campos também não eram consistentes: alguns usavam created_at, alguns usavam createdAt, e outros usavam createdDate. Senhas e IDs internos também eram expostos no JSON. Havia mais código de adaptação no front-end do que lógica de negócio.
(2) Solução com Recursos de API
Os Recursos de API controlam uniformemente o formato de saída JSON—garantindo nomes de campos consistentes, ocultando campos sensíveis, aninhando associações condicionais e aderindo a formatos padrão de paginação.
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'status' => $this->status,
'products' => ProductResource::collection($this->whenLoaded('products')),
'created_at' => $this->created_at->toISOString(),
];
}
}
(3) Resultado
Depois que Alice começou a usar Recursos de API, todos os endpoints foram padronizados, o código de adaptação do front-end foi reduzido em 80%, e campos sensíveis como senhas foram automaticamente ocultados.
3. A Classe Resource e ResourceCollection
(1) Criar um Resource
php artisan make:resource ShopResource
php artisan make:resource ProductResource
php artisan make:resource OrderResource
# Cria: app/Http/Resources/ShopResource.php
(2) Classe Resource (Registro Único)
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->description,
'status' => $this->status,
'revenue' => (float) $this->revenue,
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
];
}
}
// Uso — recurso único
return new ShopResource($shop);
// {"data": {"id": 1, "name": "Alice Store", ...}}
(3) ResourceCollection (Múltiplos Registros)
// Usando coleção de recursos
return ShopResource::collection($shops);
// {"data": [...], "links": {...}, "meta": {...}}
// Coleção personalizada
php artisan make:resource ShopCollection
class ShopCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total_shops' => $this->collection->count(),
],
];
}
}
| Tipo | Formato de Retorno | Adequado Para |
|---|---|---|
new Resource($model) |
{data: {...}} |
Registro Único |
Resource::collection($models) |
{data: [...], links, meta} |
Lista + Paginação |
CustomCollection |
Formato Personalizado | Requer meta adicional |
(1) ▶ Exemplo: ShopResource do ShopMetrics
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->whenNotNull($this->description),
'status' => $this->status,
'revenue' => [
'raw' => (float) $this->revenue,
'formatted' => $this->revenue_formatted,
],
'products_count' => $this->whenCounted('products'),
'orders_count' => $this->whenCounted('orders'),
'products' => ProductResource::collection($this->whenLoaded('products')),
'latest_order' => new OrderResource($this->whenLoaded('latestOrder')),
'links' => [
'self' => route('api.v1.shops.show', $this->id),
'products' => route('api.v1.products.index', ['shop_id' => $this->id]),
],
'created_at' => $this->created_at->toISOString(),
];
}
}
Saída:
// Execução bem-sucedida
4. Recursos Relacionados Aninhados
(1) Carregamento Condicional com whenLoaded()
// Incluir relação apenas se foi carregada antecipadamente
'products' => ProductResource::collection($this->whenLoaded('products')),
// Se products não foram carregados com(), isso retorna null e é omitido
// Shop::find(1) → sem chave products no JSON
// Shop::with('products')->find(1) → products incluídos
// Relação única
'user' => new UserResource($this->whenLoaded('user')),
// Apenas contagem (sem dados)
'products_count' => $this->whenCounted('products'),
(2) Definindo Recursos Aninhados
// app/Http/Resources/OrderResource.php
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'order_number' => $this->order_number,
'status' => $this->status,
'total' => (float) $this->total,
'items' => OrderItemResource::collection($this->whenLoaded('items')),
'shop' => new ShopBriefResource($this->whenLoaded('shop')),
'user' => new UserBriefResource($this->whenLoaded('user')),
'created_at' => $this->created_at->toISOString(),
];
}
}
// Recurso breve — dados mínimos para aninhamento
class ShopBriefResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
];
}
}
| Método | Descrição | Como Evitar Problemas |
|---|---|---|
whenLoaded() |
Saída apenas após as junções terem sido carregadas | Evitar N+1 com carregamento preguiçoso |
whenCounted() |
Saída apenas após contagem | Evitar consultas extras |
whenNotNull() |
Saída apenas se não for nulo | Limpar campos vazios |
when() |
Saída Condicional | Controle Flexível |
| Recurso Breve | Usar a Versão Lite para Aninhamento | Evitar Loops Aninhados |
(1) ▶ Exemplo: Recursos Aninhados do ShopMetrics
// app/Http/Resources/OrderItemResource.php
class OrderItemResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'product' => new ProductBriefResource($this->whenLoaded('product')),
'quantity' => $this->quantity,
'unit_price' => (float) $this->price,
'subtotal' => (float) ($this->price * $this->quantity),
];
}
}
// app/Http/Resources/ProductBriefResource.php
class ProductBriefResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'sku' => $this->sku,
];
}
}
Saída:
// Execução bem-sucedida
5. Especificações de Design de API RESTful
(1) URIs e Verbos HTTP
| Operação | HTTP | URI | Descrição |
|---|---|---|---|
| Listar | GET | /api/v1/shops | Retorna uma coleção |
| Criar | POST | /api/v1/shops | Criar recurso |
| Detalhes | GET | /api/v1/shops/{id} | Retorna um registro único |
| Atualização Completa | PUT | /api/v1/shops/{id} | Substituir Recurso |
| Atualização Parcial | PATCH | /api/v1/shops/{id} | Modificar Campos |
| Excluir | DELETE | /api/v1/shops/{id} | Excluir recurso |
(2) Códigos de Status HTTP
| Código de Status | Significado | Caso de Uso |
|---|---|---|
| 200 | OK | Resposta bem-sucedida |
| 201 | Created | Recurso criado com sucesso |
| 204 | No Content | Excluído com sucesso |
| 400 | Bad Request | Formato de requisição inválido |
| 401 | Unauthorized | Não Autenticado |
| 403 | Forbidden | Sem Permissão |
| 404 | Not Found | Recurso não existe |
| 422 | Unprocessable Entity | Falha na Validação |
| 429 | Too Many Requests | Limite de Requisições |
| 500 | Internal Server Error | Erro do Servidor |
(3) Formato de Resposta de Erro
{
"success": false,
"message": "Validation failed.",
"errors": {
"name": ["The name field is required."],
"email": ["The email must be a valid email address."]
}
}
(1) ▶ Exemplo: Design de Endpoint de API RESTful do ShopMetrics
// routes/api.php
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
// Shops
Route::apiResource('shops', Api\V1\ShopController::class);
Route::get('shops/{shop}/analytics', [Api\V1\ShopAnalyticsController::class, 'show']);
// Products (aninhados depois rasos)
Route::apiResource('shops.products', Api\V1\ProductController::class)->shallow();
// Orders
Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show', 'update']);
Route::post('orders/{order}/cancel', [Api\V1\OrderController::class, 'cancel']);
// Categories
Route::apiResource('categories', Api\V1\CategoryController::class)->only(['index', 'show']);
// Analytics
Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview']);
});
Saída:
// Execução bem-sucedida
6. Encapsulamento de Filtragem, Ordenação e Paginação
(1) Classe Base de Filtro de Consulta
// app/Filters/QueryFilter.php
abstract class QueryFilter
{
public function __construct(protected Request $request) {}
public function apply(Builder $query): Builder
{
foreach ($this->filters() as $filter => $value) {
if (method_exists($this, $filter) && $value !== null) {
$this->$filter($query, $value);
}
}
return $query;
}
protected function filters(): array
{
return $this->request->all();
}
}
(2) Filtro Específico
// app/Filters/ShopFilter.php
class ShopFilter extends QueryFilter
{
public function search(Builder $query, string $value): Builder
{
return $query->where('name', 'like', "%{$value}%");
}
public function status(Builder $query, string $value): Builder
{
return $query->where('status', $value);
}
public function min_revenue(Builder $query, float $value): Builder
{
return $query->where('revenue', '>=', $value);
}
public function sort(Builder $query, string $value): Builder
{
$direction = str_starts_with($value, '-') ? 'desc' : 'asc';
$field = ltrim($value, '-');
return $query->orderBy($field, $direction);
}
}
(1) ▶ Exemplo: Endpoints de API do ShopMetrics com Filtros
// app/Http/Controllers/Api/V1/ShopController.php
class ShopController extends Controller
{
public function index(ShopFilter $filter): JsonResponse
{
$shops = Shop::where('tenant_id', tenant()->id)
->filter($filter)
->withCount(['products', 'orders'])
->paginate(request()->integer('per_page', 15));
return ShopResource::collection($shops);
}
public function store(StoreShopRequest $request): JsonResponse
{
$shop = Shop::create(array_merge($request->validated(), ['tenant_id' => tenant()->id]));
return response()->json([
'message' => 'Shop created.',
'data' => new ShopResource($shop),
], 201);
}
public function show(Shop $shop): JsonResponse
{
$shop->load(['products' => fn ($q) => $q->active()->latest()->take(10)]);
return new ShopResource($shop);
}
public function update(UpdateShopRequest $request, Shop $shop): JsonResponse
{
$shop->update($request->validated());
return new ShopResource($shop);
}
public function destroy(Shop $shop): Response
{
$shop->delete();
return response()->noContent();
}
}
Saída:
// Execução bem-sucedida
7. Controle de Versão de API
(1) Versão do Grupo de Rotas
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('shops', Api\V1\ShopController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('shops', Api\V2\ShopController::class);
});
(2) Herança de Recursos
// V1 ShopResource
namespace App\Http\Resources\Api\V1;
class ShopResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'status' => $this->status,
];
}
}
// V2 ShopResource — estende e adiciona campos
namespace App\Http\Resources\Api\V2;
class ShopResource extends \App\Http\Resources\Api\V1\ShopResource
{
public function toArray(Request $request): array
{
return array_merge(parent::toArray($request), [
'revenue' => (float) $this->revenue,
'products_count' => $this->whenCounted('products'),
'links' => [
'self' => route('api.v2.shops.show', $this->id),
],
]);
}
}
| Estratégia de Versionamento | Abordagem | Prós e Contras |
|---|---|---|
| Prefixo na URL | /api/v1/, /api/v2/ |
✅ Simples e Claro |
| Header | Accept: application/vnd.api.v2+json |
Mais RESTful mas Mais Complexo |
| Query | ?version=2 |
Não recomendado, não RESTful |
(1) ▶ Exemplo: Roteamento de Versão de API do ShopMetrics
// routes/api.php
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
Route::apiResource('shops', Api\V1\ShopController::class);
Route::apiResource('products', Api\V1\ProductController::class);
Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
});
Route::prefix('v2')->middleware('auth:sanctum')->group(function () {
Route::apiResource('shops', Api\V2\ShopController::class);
Route::apiResource('products', Api\V2\ProductController::class);
Route::apiResource('orders', Api\V2\OrderController::class);
// V2 adiciona CRUD completo para pedidos + analytics
Route::get('analytics', [Api\V2\AnalyticsController::class, 'overview']);
});
Saída:
// Execução bem-sucedida
8. Exemplo Abrangente: O Processo Completo para Recursos de API do ShopMetrics
// ============================================
// Abrangente: Recursos de API do ShopMetrics
// Cobre: recursos, relações, filtragem, paginação, versionamento
// ============================================
// app/Http/Resources/Api/V1/OrderResource.php
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'order_number' => $this->order_number,
'status' => $this->status,
'subtotal' => (float) $this->subtotal,
'discount' => (float) $this->discount,
'total' => (float) $this->total,
'items_count' => $this->whenCounted('items'),
'items' => OrderItemResource::collection($this->whenLoaded('items')),
'shop' => new ShopBriefResource($this->whenLoaded('shop')),
'user' => [
'id' => $this->whenLoaded('user')?->id,
'name' => $this->whenLoaded('user')?->name,
],
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
];
}
}
// Controller com pipeline completo
class OrderController extends Controller
{
public function index(OrderFilter $filter): JsonResponse
{
$orders = Order::where('tenant_id', tenant()->id)
->filter($filter)
->with(['shop', 'user'])
->withCount('items')
->latest()
->paginate(request()->integer('per_page', 15));
return OrderResource::collection($orders);
}
public function show(Order $order): JsonResponse
{
$order->load(['items.product', 'shop', 'user']);
return new OrderResource($order);
}
public function update(UpdateOrderRequest $request, Order $order): JsonResponse
{
$order->update($request->validated());
return new OrderResource($order->fresh()->load('items.product'));
}
}
❓ Perguntas Frequentes
Resource e retornar diretamente o JSON do modelo?Resource permite controle preciso sobre os campos de saída, valores formatados e relacionamentos aninhados. APIs devem usar Resource.whenLoaded e acesso direto a um relacionamento?whenLoaded só gera o relacionamento se ele foi pré-carregado; se não foi carregado, retorna null e é automaticamente removido do JSON.paginationResponse() do JsonResource::collection() para personalizar o formato de paginação.$request->user() ou auth()->user(). O método toArray() do Resource aceita um parâmetro Request, permitindo determinar quais campos exibir com base nas permissões do usuário.$this->when() em Full Resource para exibir campos dinamicamente com base no contexto.📖 Resumo
- API Resource: Controlando Exposição de Campos, Formatação e Aninhamento de Relacionamentos
- use
whenLoaded()para carregamento condicional de relacionamentos para evitar N+1 com carregamento preguiçoso - Recurso Breve é usado em cenas de aninhamento para evitar referências circulares
- APIs RESTful seguem a especificação URI + verbo HTTP + código de status
- QueryFilter encapsula lógica de filtragem e ordenação, mantendo o controller limpo
- Versões de API são indicadas por prefixos de URL (/v1/, /v2/), e recursos podem ser herdados e reutilizados
📝 Exercícios
-
Exercício Básico (⭐): Crie três classes de recurso—ShopResource, ProductResource e OrderResource—para o ShopMetrics. No controller, substitua o retorno direto do modelo para garantir formatação JSON consistente.
-
Exercício Avançado (⭐⭐): Implemente o filtro de consulta ShopFilter para suportar os parâmetros search, status, min_revenue e sort. Use-o em ShopController::index e teste a funcionalidade de filtragem usando Postman.
-
Desafio (⭐⭐⭐): Desenvolva duas versões de API, V1 e V2—V1 retorna apenas os campos básicos, enquanto V2 retorna adicionalmente revenue, products_count e links. Implemente isso usando herança de recursos, garantindo que o endpoint V1 não quebre os clientes existentes.



