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
-
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. -
Implemente autenticação com Sanctum: endpoints de login, logout e
/api/me. Tokens com expiração de 24h. Proteja todas as rotas de escrita. -
Configure o handler de erros para retornar JSON consistente em todos os cenários (404, 401, 403, 422, 500) com a estrutura
{ mensagem, erros? }. -
Desafio: implemente versionamento de API:
v1ev2com namespaces e prefixos separados. Nov2, adicione um campodescricao_curtanoProdutoResource(primeiros 100 chars da descrição) sem alterar ov1.
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