404 Not Found

404 Not Found


nginx

Requisições e Respostas do Laravel

Requisições e respostas são as "entradas e saídas" do Laravel—dados são extraídos quando uma requisição entra, e dados são empacotados quando uma resposta sai; ambos os processos exigem controle preciso.

1. O Que Você Vai Aprender


2. Uma História Real de um Desenvolvedor de API

(1) Problema: Cada endpoint retorna dados em formato diferente

Bob escreveu 10 endpoints para a API do ShopMetrics—alguns retornam {data: [...]}, alguns retornam {shops: [...]}, e outros retornam arrays diretamente. Ao integrar a API no frontend, Alice teve que escrever lógica de análise diferente para cada endpoint. Para piorar, dados paginados eram retornados como total_pages ou last_page, impossibilitando a padronização do componente de paginação do frontend.

(2) Métodos para Resolver Respostas Padronizadas

O API Resource e as macros de resposta do Laravel padronizam o formato de saída—todos os endpoints retornam a mesma estrutura JSON, e dados paginados seguem um formato consistente.

PHP
// Resposta padronizada — todo endpoint segue este formato
return ShopResource::collection($shops);
// {
//   "data": [...],
//   "meta": {"current_page": 1, "total": 50},
//   "links": {"next": "...", "prev": "..."}
// }

(3) Resultado

Após Alice adotar um formato unificado, o número de conjuntos de lógica de análise do frontend foi reduzido de 10 para 1, e a taxa de reutilização do componente de paginação atingiu 100%.


3. O Objeto Request

(1) Obter Entrada

PHP
// Obter entrada única
$name = $request->input('name');
$name = $request->input('name', 'valor padrão');

// Obter apenas da string de consulta
$sort = $request->query('sort', 'created_at');

// Obter múltiplas entradas
$filtered = $request->only(['name', 'email', 'status']);
$filtered = $request->except(['password', '_token']);

// Verificar existência
if ($request->has('search')) { ... }
if ($request->filled('search')) { ... }  // tem + não vazio
if ($request->missing('search')) { ... }

// Conversão de tipo
$page = $request->integer('page', 1);
$active = $request->boolean('active', false);
$date = $request->date('published_at', 'Y-m-d');
Método Descrição Diferença
input() Todas as entradas (query+body) Mais comum
query() Apenas string de consulta Parâmetros GET
post() Apenas body POST Dados de formulário
only() Extrair Campos Especificados Lista Branca
except() Excluir Campos Especificados Lista Negra
has() Campo existe Inclui valores null
filled() Campo existe e não está vazio Exclui valores null

(1) ▶ Exemplo: Processando Requisições de API do ShopMetrics

PHP
// app/Http/Controllers/Api/ShopController.php
public function index(Request $request): JsonResponse
{
    $query = Shop::where('tenant_id', tenant()->id);

    // Filtro de busca
    if ($request->filled('search')) {
        $query->where('name', 'like', "%{$request->search}%");
    }

    // Filtro de status
    if ($request->filled('status')) {
        $query->where('status', $request->status);
    }

    // Ordenação
    $sortField = $request->input('sort', 'created_at');
    $sortDir = $request->input('direction', 'desc');
    $query->orderBy($sortField, $sortDir);

    // Paginação
    $perPage = $request->integer('per_page', 15);
    $shops = $query->paginate($perPage);

    return ShopResource::collection($shops);
}

Saída:

TEXT
// Execução bem-sucedida

4. Upload de Arquivos

(1) Processamento Básico de Upload

PHP
// Validar upload de arquivo
$validated = $request->validate([
    'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);

// Armazenar arquivo
$path = $request->file('logo')->store('shops/logos', 'public');
// => "shops/logos/abc123.jpg"

// Armazenar com nome personalizado
$path = $request->file('logo')->storeAs(
    'shops/logos',
    $shop->slug . '.' . $request->file('logo')->extension(),
    'public',
);

// Obter informações do arquivo
$file = $request->file('logo');
$file->getClientOriginalName();
$file->getClientOriginalExtension();
$file->getSize();          // bytes
$file->getMimeType();
Método Descrição
store() Salvar no diretório especificado com nome de arquivo aleatório
storeAs() Salvar no diretório especificado com nome de arquivo personalizado
storePublicly() Salvar no diretório público
isValid() Verificar se o upload foi bem-sucedido

(1) ▶ Exemplo: Upload do Logo da Loja do ShopMetrics

PHP
// app/Http/Controllers/ShopLogoController.php
class ShopLogoController extends Controller
{
    public function update(Request $request, Shop $shop): RedirectResponse
    {
        $validated = $request->validate([
            'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
        ]);

        // Excluir logo antigo se existir
        if ($shop->logo_path) {
            Storage::disk('public')->delete($shop->logo_path);
        }

        // Armazenar novo logo
        $path = $request->file('logo')->store("shops/{$shop->id}/logos", 'public');

        $shop->update(['logo_path' => $path]);

        return back()->with('success', 'Logo atualizado.');
    }
}

Saída:

TEXT
// Execução bem-sucedida

5. Construtor de Resposta

(1) Tipo de Resposta

PHP
// Resposta JSON
return response()->json(['message' => 'Criado', 'data' => $shop], 201);

// Resposta de View
return response()->view('shops.show', compact('shop'), 200);

// Redirecionamento
return redirect()->route('shops.show', $shop);
return back()->withInput()->withErrors($errors);
return redirect()->away('https://external-site.com');

// Download de arquivo
return response()->download(storage_path('app/reports/report.pdf'));
return response()->streamDownload(function () {
    echo generateCsv();
}, 'orders.csv', ['Content-Type' => 'text/csv']);

// Sem conteúdo
return response()->noContent(); // 204
Tipo de Resposta Método Código de Status HTTP
JSON response()->json() 200/201/422
View response()->view() 200
Redirecionamento redirect() 302
Download response()->download() 200
Download Stream response()->streamDownload() 200
Sem conteúdo response()->noContent() 204

(1) ▶ Exemplo: Resposta de Streaming de Exportação CSV do ShopMetrics

PHP
// app/Http/Controllers/ExportController.php
class ExportController extends Controller
{
    public function exportOrders(Request $request, Shop $shop): StreamedResponse
    {
        $this->authorize('view', $shop);

        return response()->streamDownload(function () use ($shop) {
            $csv = fopen('php://output', 'w');
            fputcsv($csv, ['ID do Pedido', 'Cliente', 'Total', 'Status', 'Data']);

            $shop->orders()
                ->with('user')
                ->orderBy('created_at', 'desc')
                ->chunk(500, function ($orders) use ($csv) {
                    foreach ($orders as $order) {
                        fputcsv($csv, [
                            $order->order_number,
                            $order->user->name,
                            $order->total,
                            $order->status,
                            $order->created_at->format('Y-m-d'),
                        ]);
                    }
                });

            fclose($csv);
        }, "orders-{$shop->slug}-" . now()->format('Y-m-d') . '.csv', [
            'Content-Type' => 'text/csv',
        ]);
    }
}

Saída:

TEXT
// Execução bem-sucedida

6. Paginação de API

(1) Comparação de Métodos de Paginação

Método Número de Consultas Informação Retornada Casos de Uso
paginate() 2 (COUNT+SELECT) total/last_page/links Número total de páginas necessário
simplePaginate() 1 (SELECT) links next/prev Número total de páginas não necessário
cursorPaginate() 1 (SELECT+WHERE) cursor next/prev Grande conjunto de dados

(2) Paginação em Formato JSON

JSON
{
    "data": [
        {"id": 1, "name": "Alice Store"},
        {"id": 2, "name": "Bob Electronics"}
    ],
    "current_page": 1,
    "per_page": 15,
    "total": 50,
    "last_page": 4,
    "from": 1,
    "to": 15,
    "links": {
        "first": "/api/v1/shops?page=1",
        "last": "/api/v1/shops?page=4",
        "next": "/api/v1/shops?page=2",
        "prev": null
    }
}

(3) Paginação por Cursor

PHP
// Mais eficiente para grandes conjuntos de dados
$shops = Shop::cursorPaginate(15);
// Usa parâmetro "cursor" em vez de "page"
// URL: /api/shops?cursor=eyJpZCI6MTV9

// URL da próxima página
$shops->nextPageUrl();
// /api/shops?cursor=eyJpZCI6MzB9

(1) ▶ Exemplo: Paginação e Filtragem de API do ShopMetrics

PHP
// app/Http/Controllers/Api/OrderController.php
public function index(Request $request): JsonResponse
{
    $query = Order::where('tenant_id', tenant()->id)
        ->with(['shop', 'user', 'items.product']);

    // Filtros
    if ($request->filled('status')) {
        $query->where('status', $request->status);
    }
    if ($request->filled('shop_id')) {
        $query->where('shop_id', $request->shop_id);
    }
    if ($request->filled('date_from')) {
        $query->where('created_at', '>=', $request->date('date_from'));
    }
    if ($request->filled('date_to')) {
        $query->where('created_at', '<=', $request->date('date_to'));
    }

    // Ordenação
    $query->orderBy(
        $request->input('sort_by', 'created_at'),
        $request->input('sort_dir', 'desc'),
    );

    // Escolher estratégia de paginação
    $perPage = $request->integer('per_page', 15);

    $orders = $request->boolean('cursor', false)
        ? $query->cursorPaginate($perPage)
        : $query->paginate($perPage);

    return OrderResource::collection($orders);
}

Saída:

TEXT
// Execução bem-sucedida

7. Macros de Resposta e Padronização de Formato

(1) Definir uma macro de resposta

PHP
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    Response::macro('apiSuccess', function (mixed $data, string $message = 'Sucesso', int $status = 200) {
        return response()->json([
            'success' => true,
            'message' => $message,
            'data' => $data,
        ], $status);
    });

    Response::macro('apiError', function (string $message, int $status = 400, array $errors = []) {
        return response()->json([
            'success' => false,
            'message' => $message,
            'errors' => $errors,
        ], $status);
    });
}

(2) Usando Macros de Resposta

PHP
// No controlador
return response()->apiSuccess($shop, 'Loja criada', 201);
return response()->apiError('Loja não encontrada', 404);
return response()->apiError('Validação falhou', 422, $validator->errors()->toArray());

(1) ▶ Exemplo: Formato de Resposta Padrão da API do ShopMetrics

PHP
// app/Traits/ApiResponse.php
trait ApiResponse
{
    protected function success(mixed $data, string $message = 'Sucesso', int $status = 200): JsonResponse
    {
        return response()->json([
            'success' => true,
            'message' => $message,
            'data' => $data,
            'timestamp' => now()->toISOString(),
        ], $status);
    }

    protected function error(string $message, int $status = 400, array $errors = []): JsonResponse
    {
        return response()->json([
            'success' => false,
            'message' => $message,
            'errors' => $errors,
            'timestamp' => now()->toISOString(),
        ], $status);
    }

    protected function paginated($resource, string $message = 'Sucesso'): JsonResponse
    {
        return response()->json([
            'success' => true,
            'message' => $message,
            'data' => $resource->resolve(),
            'meta' => [
                'current_page' => $resource->resource->currentPage(),
                'per_page' => $resource->resource->perPage(),
                'total' => $resource->resource->total(),
                'last_page' => $resource->resource->lastPage(),
            ],
            'links' => [
                'first' => $resource->resource->url(1),
                'last' => $resource->resource->url($resource->resource->lastPage()),
                'next' => $resource->resource->nextPageUrl(),
                'prev' => $resource->resource->previousPageUrl(),
            ],
        ]);
    }
}

Saída:

TEXT
// Execução bem-sucedida

8. Exemplo Completo: O Processo Completo de Tratamento de Requisições de API do ShopMetrics

PHP
// ============================================
// Completo: Requisição-Resposta de API do ShopMetrics
// Abrange: tratamento de entrada, upload de arquivo, paginação, formato de resposta
// ============================================

// app/Http/Controllers/Api/ProductController.php
class ProductController extends Controller
{
    use ApiResponse;

    public function index(Request $request): JsonResponse
    {
        $query = Product::where('shop_id', $request->shop_id)
            ->with('categories');

        if ($request->filled('search')) {
            $query->where('name', 'like', "%{$request->search}%");
        }
        if ($request->filled('category')) {
            $query->whereHas('categories', fn ($q) => $q->where('slug', $request->category));
        }
        if ($request->filled('min_price')) {
            $query->where('price', '>=', $request->float('min_price'));
        }
        if ($request->filled('max_price')) {
            $query->where('price', '<=', $request->float('max_price'));
        }
        if ($request->filled('in_stock') && $request->boolean('in_stock')) {
            $query->where('stock', '>', 0);
        }

        $query->orderBy(
            $request->input('sort', 'created_at'),
            $request->input('direction', 'desc'),
        );

        $products = $query->paginate($request->integer('per_page', 15));

        return $this->paginated(ProductResource::collection($products));
    }

    public function store(StoreProductRequest $request): JsonResponse
    {
        $product = Product::create($request->validated());

        if ($request->hasFile('image')) {
            $path = $request->file('image')->store("products/{$product->id}", 's3');
            $product->update(['image_path' => $path]);
        }

        return $this->success(new ProductResource($product), 'Produto criado', 201);
    }

    public function export(Request $request, Shop $shop): StreamedResponse
    {
        $this->authorize('view', $shop);

        return response()->streamDownload(function () use ($shop) {
            echo ProductExporter::toCsv($shop);
        }, "products-{$shop->slug}.csv", ['Content-Type' => 'text/csv']);
    }
}

❓ Perguntas Frequentes

P Qual é a diferença entre input() e query()?
R input() recupera dados de todas as fontes (string de consulta + corpo da requisição), enquanto query() recupera dados apenas da string de consulta da URL. É mais claro usar input() para dados de formulário POST e query() para parâmetros GET.
P Como escolher entre cursorPaginate e paginate?
R Use paginate para conjuntos de dados menores que 100.000 (requer exibição do número total de páginas); use cursorPaginate para conjuntos de dados maiores que 100.000 (sem consulta COUNT necessária, melhor desempenho). UI de rolagem infinita é mais adequada para cursor.
P Como o formato de resposta da API deve ser padronizado?
R Crie um trait ApiResponse ou macro de resposta para encapsular os métodos success, error e paginated. Todos os controladores devem usar esses métodos de resposta padronizados para garantir consistência em nomes de campos, códigos de status e formatos de paginação.
P Devo usar download() ou streamDownload() para baixar arquivos grandes?
R Para arquivos pequenos, use download() para lê-los diretamente na memória; para arquivos grandes, use streamDownload() para streaming, que mantém o uso de memória constante. streamDownload() deve ser usado para cenários como exportação CSV.
P Como lidar com diferenças nos formatos de resposta entre versões de API?
R Cada versão de API usa uma classe Resource separada (V1/ShopResource vs. V2/ShopResource); os campos de saída são controlados no método toArray() do Resource, e a camada de roteamento seleciona a versão apropriada.
P Qual é a diferença entre armazenar arquivos carregados localmente e no S3?
R O armazenamento local está no disco do servidor (gratuito mas não escalável), enquanto o S3 está na nuvem (pagamento por uso, aceleração CDN, escalabilidade ilimitada). S3 é recomendado para ambientes de produção.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Implemente o endpoint GET /api/v1/shops para a API do ShopMetrics, suportando os parâmetros de filtro search, status, sort e direction, e use paginate para retornar JSON em blocos paginados.

  2. Exercício Avançado (⭐⭐): Crie um trait ApiResponse que encapsule os métodos success, error e paginated, e use-o em todos os controladores de API para garantir um formato de resposta consistente.

  3. Desafio (⭐⭐⭐): Implemente uma API de lista de pedidos paginada por cursor (GET /api/v1/orders?cursor=xxx), e compare as diferenças de desempenho de consulta entre paginate e cursorPaginate ao processar 100.000 registros.

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%