404 Not Found

404 Not Found


nginx

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


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.

PHP
// 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

BASH
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)

PHP
// 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)

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

4. Recursos Relacionados Aninhados

(1) Carregamento Condicional com whenLoaded()

PHP
// 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

PHP
// 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

PHP
// 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:

TEXT
// 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

JSON
{
    "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

PHP
// 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:

TEXT
// Execução bem-sucedida

6. Encapsulamento de Filtragem, Ordenação e Paginação

(1) Classe Base de Filtro de Consulta

PHP
// 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

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

7. Controle de Versão de API

(1) Versão do Grupo de Rotas

PHP
// 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

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

8. Exemplo Abrangente: O Processo Completo para Recursos de API do ShopMetrics

PHP
// ============================================
// 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

P Qual é a diferença entre usar Resource e retornar diretamente o JSON do modelo?
R Retornar o modelo diretamente expõe todos os campos (incluindo dados sensíveis como senhas), e o formato não pode ser controlado; Resource permite controle preciso sobre os campos de saída, valores formatados e relacionamentos aninhados. APIs devem usar Resource.
P Qual é a diferença entre whenLoaded e acesso direto a um relacionamento?
R O acesso direto a um relacionamento aciona o carregamento preguiçoso (o problema N+1); whenLoaded só gera o relacionamento se ele foi pré-carregado; se não foi carregado, retorna null e é automaticamente removido do JSON.
P Quantas versões da API são necessárias?
R Normalmente, apenas duas versões são mantidas (a atual e a anterior). Quando uma nova versão é lançada, é dado aos usuários um período de migração de 6 meses; após esse período expirar, a versão antiga retorna um status 410 Gone. Evite manter muitas versões ao mesmo tempo.
P O formato de paginação para ResourceCollection pode ser personalizado?
R Sim. Crie uma classe Collection personalizada e sobrescreva o método toArray(), ou use o método paginationResponse() do JsonResource::collection() para personalizar o formato de paginação.
P Como acessar usuários autenticados em um Resource?
R Use $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.
P O que devo fazer se houver código duplicado entre Brief Resource e Full Resource?
R Faça Brief Resource herdar de Full Resource e sobrescreva apenas toArray() para exibir menos campos; ou use $this->when() em Full Resource para exibir campos dinamicamente com base no contexto.

📖 Resumo


📝 Exercícios

  1. 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.

  2. 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.

  3. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%