Versionamento de APIs com Laravel: rotas, headers, DTOs e compatibilidade
Por John Pires · 18 de março de 2026

Versionamento de APIs com Laravel: rotas, headers, DTOs e compatibilidade
Contexto e motivacao
Problema prático
Projetos de API crescem. As mudanças inevitavelmente quebram consumidores se não forem gerenciadas. Equipes pequenas frequentemente usam hacks (mudar contrato, lançar nova rota ad-hoc) e acumulam débito técnico que aumenta o custo de manutenção. Versionamento planejado reduz risco, dá previsibilidade e permite decompor mudanças grandes em rollout controlado.
Laravel facilita expor múltiplas versões via rotas e middleware, mas requer disciplina: rotas, cabeçalhos, DTOs e contratos de serviço precisam coordenar-se. Sem padrão, você terá duplicação, código condicional espalhado e endpoints inconsistentes.
Requisitos funcionais
Antes de escolher uma estratégia, defina requisitos claros: tempo de vida das versões, política de migração, estratégia de rollback e métrica de uso por versão. Essas decisões influenciam se você usará versionamento por URI, header ou media-type.
Na prática, você deve poder: 1) manter duas versões simultâneas; 2) identificar versão por cliente; 3) evoluir internamente sem impactar outras versões; 4) testar e liberar mudanças gradualmente.
Como funciona por baixo dos panos
Versionamento por rota vs header
Existem dois padrões óbvios: URI (ex: /api/v1/users) e header (ex: Accept: application/vnd.myapp.v2+json ou X-Api-Version: 2). URI é simples e explícito — fácil de cache e de debugar. Header mantém URIs limpas e permite roteamento baseado em conteúdo. Ambos exigem uma camada de resolução de versão para mapear requisições à implementação correta.
Trade-offs: URI facilita proxies e documentação; header permite múltiplas representações no mesmo URI. Escolha com base nos requisitos de clientes e infra.
Resolução de versão e fallback
A resolução de versão é trivial em teoria: ler header/URI/param e mapear. Na prática, implemente fallback e validação. Se o cliente não informar versão, defina um default (normalmente a versão estável mais antiga) e registre métricas. Evite comportamento implícito: registre logs e retorne um header de resposta indicando a versão escolhida.
Internamente, a resolução alimenta três mecanismos: roteamento (qual controller/serviço usar), serialização (DTOs/Resources) e validação (FormRequests). Centralize essa resolução em um middleware ou serviço resolvível por container para evitar ifs espalhados.
Implementacao passo a passo
Estrutura de rotas
Estruture routes/api.php com grupos claros por versão. Isso mantém controllers por versão isolados e facilita deploy paralelo.
Exemplo: rotas simples que apontam para controllers separados por versão. O código abaixo cria os grupos e aponta para controllers distintos.
<?php
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => 'v1', 'middleware' => ['api']], function () {
Route::get('users', [App\Http\Controllers\Api\V1\UserController::class, 'index']);
Route::get('users/{id}', [App\Http\Controllers\Api\V1\UserController::class, 'show']);
});
Route::group(['prefix' => 'v2', 'middleware' => ['api']], function () {
Route::get('users', [App\Http\Controllers\Api\V2\UserController::class, 'index']);
Route::get('users/{id}', [App\Http\Controllers\Api\V2\UserController::class, 'show']);
});
O que faz: cria dois namespaces de rota, v1 e v2. Depois: cada controller pode evoluir independentemente; caches HTTP funcionam normalmente com URIs versionadas.
Impacto: documentação e clientes ficam explícitos. Contras: duplicação de rota e potencial duplicação de lógica se você não extrair serviços compartilhados.
Middleware de resolução de versão
Se preferir header-based, implemente um middleware que analisa Accept ou um header customizado e sufoca lógica condicional nas controllers. Exemplo funcional abaixo.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class VersionResolver
{
public function handle(Request $request, Closure $next)
{
$version = null;
if ($request->headers->has('X-Api-Version')) {
$version = $request->header('X-Api-Version');
} elseif ($accept = $request->header('Accept')) {
if (preg_match('#vnd\.myapp\.v([0-9]+)\+json#', $accept, $m)) {
$version = 'v'.$m[1];
}
}
$request->attributes->set('api_version', $version ?? 'v1');
// opcional: acrescentar header de resposta para debug
$response = $next($request);
$response->headers->set('X-Api-Selected-Version', $request->attributes->get('api_version'));
return $response;
}
}
O que faz: define atributo request->attributes['api_version'] e adiciona X-Api-Selected-Version na resposta. Depois: todos os componentes podem consultar a versão sem ler headers manualmente.
Observação: escape do regex e verificação adequada são importantes para evitar falsos positivos. Logue requisições sem versão para entender impacto ao descontinuar v1.
Binding do resolvedor e service provider
Prefira injetar um resolvedor de versão via container, assim você pode trocar implementação sem mudar middleware. Exemplo: interface + implementação e binding no provider.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\ApiVersionResolver;
use App\Services\HeaderVersionResolver;
class VersionServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(ApiVersionResolver::class, function () {
return new HeaderVersionResolver();
});
}
}
Contexto: o HeaderVersionResolver encapsula a lógica de leitura de headers/URI. Depois: controllers recebem ApiVersionResolver via DI e tomam decisões se necessário.
Benefício: facilita testes unitários e mudanças de estratégia (podemos trocar para URI resolver sem alterar controllers).
Patterns e boas praticas
DTOs e validação
Use DTOs para separar payloads externos do domínio interno. DTOs tornam contratos explícitos e facilitam transformação entre versões. Crie factories fromRequest para conversão e mantenha validação em FormRequest ou validador dedicado.
<?php
namespace App\DTO;
final class UserDTO
{
public function __construct(
public int $id,
public string $name,
public ?string $email = null,
) {}
public static function fromRequest(array $data): self
{
return new self(
id: (int) ($data['id'] ?? 0),
name: (string) ($data['name'] ?? ''),
email: $data['email'] ?? null,
);
}
}
O que faz: encapsula campos esperados. Depois: controllers e services trabalham com objetos tipados, eliminando arrays e magic keys. Isso reduz bugs e facilita refactor.
Recomendação prática: mantenha DTOs imutáveis (readonly properties ou não alterar depois de construir). Quando necessário, use mappers para transformar entre DTOs de v1 e v2.
Compatibilidade e contratos
Separe a interface (contrato) da implementação. Exponha interfaces para serviços que implementam comportamento por versão. Isso permite fallback e testes A/B sem duplicar lógica.
Use adapter pattern quando a diferença entre versões for apenas transformacional. Extraia lógica compartilhada em serviços stateless consumidos por múltiplos controllers de versão.
- Extraia validações comuns em Request classes reutilizáveis.
- Use Resources (JsonResource) para serialização por versão.
- Mantenha tests por versão (integração + contract tests).
Armadilhas comuns e como evitar
Quebrar compatibilidade sem versionar
Erro comum: alterar contrato de resposta sem bump de versão. Resultado: clientes falhando em produção. Evite alterando comportamento de campos públicos (renomear, mudar tipo, remover). Se mudar, crie nova versão ou mantenha compat layer que transforma novo formato para antigo.
Se uma mudança pode gerar NullPointer/TypeError em clientes existentes, ela exige versionamento.
Dica prática: adicione testes de regressão que simulam consumo por versão antiga; esses testes detectam impactos inesperados.
Exposição de múltiplas versões e custo de manutenção
Manter muitas versões ativas aumenta custo. Estabeleça uma política clara (por ex. suporte 12 meses). Automatize métricas: registre uso por versão e gere reports para decidir quando deprecar. Deprecação sem oferta de caminho de migração causa churn e tickets.
Outra armadilha: copiar/colar controllers entre versões. Em vez disso, extraia serviços e adapte apenas a borda (controllers/Resources). Isso reduz duplicação e facilita correções de segurança.
Headers e proxies
Quando usar header-based versioning, verifique se proxies e caches respeitam headers customizados. Configure Vary headers corretamente (Vary: Accept, X-Api-Version) para evitar caching incorreto.
Conclusao pratica
Versionamento não é apenas técnica: é contrato com consumidores. Em Laravel, combine rotas versionadas para maior clareza com middleware/service resolver para flexibilidade. Use DTOs e interfaces para isolar mudanças internas e extrair lógica compartilhada em serviços.
Checklist rápido:
- Escolha URI ou header com base em proxies, cache e UX.
- Centralize resolução de versão em middleware ou serviço injetável.
- Use DTOs imutáveis e Resources para serialização por versão.
- Extraia serviços comuns para evitar duplicação.
- Implemente métricas e política de deprecação.
Implementando essas práticas você reduz risco ao evoluir APIs, mantém código organizado e cria caminho claro de migração para clientes. Comece aplicando middleware de resolução e extraindo um DTO por recurso: pequenos passos que pagam dividendos rapidamente.
Para referência: implemente tests contractuais por versão e um job que coleta uso por versão. Esses dados tornam a descontinuação de versões uma decisão baseada em evidências, não em achismo.
Assuntos: Laravel, PHP, api-versioning, dto, headers
Leia também
Webhooks seguros no Laravel: HMAC, idempotência e teste com ngrok
Como proteger endpoints de webhook no Laravel: validar HMAC, garantir idempotência com tabela e unique constraint, e testar localmente com ngrok. Guia prático…
APIs type-safe no Laravel: DTOs tipados, Value Objects e validação centralizada
Implementação prática para APIs type-safe em Laravel: DTOs readonly, Value Objects (Email, Money), validação centralizada com FormRequest e resolução…
Autenticação de API com Laravel Sanctum para SPA e mobile — passo a passo
Guia prático para autenticar SPA e apps mobile com Laravel Sanctum: compare o jeito errado (token no localStorage) com o certo (cookies para SPA, tokens com…
Circuit breaker para APIs externas com Laravel Http Client e Redis
Implementação prática de circuit breaker em Laravel usando Redis e Http Client. Mostra o jeito errado (flag em memória, retries inline) e a solução robusta…
ETags e cache condicional em APIs Laravel (PHP 8.3): reduzir banda e latência
Como implementar ETags e cache condicional em APIs Laravel (PHP 8.3) para reduzir banda e latência: patterns práticos, middleware, fingerprint por coleção e…
Laravel 10: consumir APIs externas com Http Client, retries, fallback e cache
Guia prático para iniciantes em Laravel 10 sobre como consumir APIs externas usando Http Client: timeout, retries, cache e fallback, com exemplos errados e…