PHP

Artigo 45 — APIs com Laravel Já leu

6 min de leitura

Artigo 45 — APIs com Laravel
Construir uma API com Laravel puro é possível — mas o framework oferece ferramentas específicas que tornam o processo mais organizado e profissional: API

Artigo 45 — APIs com Laravel

Módulo 7 · Semana 45 · Nível: Intermediário–Avançado


Introdução

Construir uma API com Laravel puro é possível — mas o framework oferece ferramentas específicas que tornam o processo mais organizado e profissional: API Resources para formatar respostas, Sanctum para autenticação stateless, rate limiting nativo, e convenções que tornam sua API previsível para quem a consome.


Configurando rotas de API

<?php
// routes/api.php — rotas aqui são prefixadas com /api automaticamente

use App\Http\Controllers\Api\ProdutoController;
use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;

// Rotas públicas
Route::post('/auth/login',    [AuthController::class, 'login']);
Route::post('/auth/register', [AuthController::class, 'register']);
Route::get('/produtos',       [ProdutoController::class, 'index']);
Route::get('/produtos/{produto}', [ProdutoController::class, 'show']);

// Rotas protegidas com Sanctum
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/auth/logout', [AuthController::class, 'logout']);
    Route::apiResource('pedidos', PedidoController::class);
    Route::post('/produtos', [ProdutoController::class, 'store']);
    Route::put('/produtos/{produto}', [ProdutoController::class, 'update']);
    Route::delete('/produtos/{produto}', [ProdutoController::class, 'destroy']);
});

API Resources — formatando respostas

Resources transformam models em arrays JSON, desacoplando a estrutura da resposta da estrutura do banco:

php artisan make:resource ProdutoResource
php artisan make:resource ProdutoCollection
<?php
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProdutoResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'          => $this->id,
            'nome'        => $this->nome,
            'slug'        => $this->slug,
            'preco'       => (float) $this->preco,
            'preco_formatado' => $this->preco_formatado,
            'estoque'     => $this->estoque,
            'disponivel'  => $this->estoque > 0 && $this->ativo,

            // Relacionamento — só inclui se foi carregado (evita N+1)
            'categoria' => new CategoriaResource($this->whenLoaded('categoria')),

            // Campo condicional — só para admins
            'custo' => $this->when(
                $request->user()?->isAdmin(),
                $this->custo
            ),

            'criado_em'   => $this->created_at->toIso8601String(),
            'atualizado_em' => $this->updated_at->toIso8601String(),
        ];
    }
}

// Collection com metadados extras
class ProdutoCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total'     => $this->total(),
                'por_pagina'=> $this->perPage(),
                'pagina'    => $this->currentPage(),
            ],
        ];
    }
}

Controller de API

<?php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\ProdutoResource;
use App\Models\Produto;
use App\Http\Requests\ProdutoRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class ProdutoController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        $produtos = Produto::with('categoria')
            ->ativo()
            ->paginate(15);

        return ProdutoResource::collection($produtos);
    }

    public function show(Produto $produto): ProdutoResource
    {
        $produto->load('categoria', 'avaliacoes');
        return new ProdutoResource($produto);
    }

    public function store(ProdutoRequest $request): JsonResponse
    {
        $produto = Produto::create($request->validated());

        return (new ProdutoResource($produto))
            ->response()
            ->setStatusCode(201);
    }

    public function update(ProdutoRequest $request, Produto $produto): ProdutoResource
    {
        $this->authorize('update', $produto);
        $produto->update($request->validated());
        return new ProdutoResource($produto->fresh('categoria'));
    }

    public function destroy(Produto $produto): JsonResponse
    {
        $this->authorize('delete', $produto);
        $produto->delete();
        return response()->json(null, 204);
    }
}

Autenticação com Sanctum

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
<?php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'email'   => ['required', 'email'],
            'senha'   => ['required'],
            'device'  => ['required', 'string'],
        ]);

        if (!Auth::attempt([
            'email'    => $request->email,
            'password' => $request->senha,
        ])) {
            throw ValidationException::withMessages([
                'email' => 'Credenciais inválidas.',
            ]);
        }

        $user  = $request->user();
        $token = $user->createToken(
            name:       $request->device,
            abilities:  ['*'],
            expiresAt:  now()->addDays(30),
        )->plainTextToken;

        return response()->json([
            'token'   => $token,
            'usuario' => [
                'id'    => $user->id,
                'nome'  => $user->name,
                'email' => $user->email,
            ],
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        // Revoga apenas o token atual
        $request->user()->currentAccessToken()->delete();

        return response()->json(['mensagem' => 'Logout realizado.']);
    }
}

Tratamento de erros na API

<?php
// app/Exceptions/Handler.php

use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

public function register(): void
{
    $this->renderable(function (\Throwable $e, Request $request) {
        if (!$request->expectsJson()) return null;

        return match(true) {
            $e instanceof ValidationException =>
                response()->json([
                    'mensagem' => 'Dados inválidos.',
                    'erros'    => $e->errors(),
                ], 422),

            $e instanceof ModelNotFoundException,
            $e instanceof NotFoundHttpException =>
                response()->json(['mensagem' => 'Recurso não encontrado.'], 404),

            $e instanceof AuthenticationException =>
                response()->json(['mensagem' => 'Não autenticado.'], 401),

            $e instanceof \Illuminate\Auth\Access\AuthorizationException =>
                response()->json(['mensagem' => 'Não autorizado.'], 403),

            default =>
                response()->json([
                    'mensagem' => 'Erro interno do servidor.',
                    'detalhe'  => config('app.debug') ? $e->getMessage() : null,
                ], 500),
        };
    });
}

Rate Limiting

<?php
// app/Providers/AppServiceProvider.php

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by(
            $request->user()?->id ?? $request->ip()
        );
    });

    RateLimiter::for('login', function (Request $request) {
        return [
            Limit::perMinute(5)->by($request->ip()),
            Limit::perMinute(3)->by($request->input('email')),
        ];
    });
}

// Em routes/api.php:
Route::middleware(['throttle:login'])->group(function () {
    Route::post('/auth/login', [AuthController::class, 'login']);
});

Resumo

Recurso Função
routes/api.php Rotas stateless com prefixo /api
API Resources Transforma models em JSON estruturado
Sanctum Tokens de API para apps mobile e SPA
$this->authorize() Verificação de permissão com Policy
Handler de erros Respostas JSON consistentes para exceções
Rate Limiting Proteção contra abuso com throttle:

Exercício da semana

  1. Construa uma API RESTful completa para o recurso Produto: CRUD com validação via FormRequest, API Resource com campos condicionais por role, paginação com metadados.

  2. Implemente autenticação com Sanctum: endpoints de login, logout e /api/me. Tokens com expiração de 24h. Proteja todas as rotas de escrita.

  3. Configure o handler de erros para retornar JSON consistente em todos os cenários (404, 401, 403, 422, 500) com a estrutura { mensagem, erros? }.

  4. Desafio: implemente versionamento de API: v1 e v2 com namespaces e prefixos separados. No v2, adicione um campo descricao_curta no ProdutoResource (primeiros 100 chars da descrição) sem alterar o v1.


Referências

  • Laravel API Resources: https://laravel.com/docs/eloquent-resources
  • Laravel Sanctum: https://laravel.com/docs/sanctum
  • Laravel Rate Limiting: https://laravel.com/docs/routing#rate-limiting
  • RESTful API Design — Melhores práticas: https://restfulapi.net/

Módulo 7 completo — 5 artigos (41–45) cobrindo Laravel do básico ao avançado.

Progresso: 45/52 artigos.

Próximo: Módulo 8 — Segurança Avançada (artigos 46–47), depois Módulo 9 — Testes (48–49), Módulo 10 — Deploy (50–51), e Artigo 52 — Projeto Final. Continuo?


you asked

Sim


claude response

Comentários

Mais em PHP

Upload de Arquivos
Upload de Arquivos

Upload de arquivos parece simples — e na superfície é. Mas quando você consid...

Artigo 47 — Testes Automatizados com PHPUnit e Pest
Artigo 47 — Testes Automatizados com PHPUnit e Pest

Testes automatizados são o seguro de vida do software. Sem eles, cada mudança...

PSR e Padrões de Código
PSR e Padrões de Código

PSR significa PHP Standards Recommendation — recomendações criadas pelo PHP-F...