API versioning e backwards compatibility
1. Introdução ao Versionamento de APIs em PHP
Versionamento de API é a prática de gerenciar mudanças em interfaces públicas sem quebrar consumidores existentes. Em ecossistemas PHP, especialmente com Laravel, isso é crucial porque aplicações frequentemente expõem endpoints para múltiplos clientes — aplicativos móveis, SPAs, integrações de terceiros — que não podem ser atualizados simultaneamente.
A falta de compatibilidade reversa em microserviços causa falhas em cascata: uma alteração em um endpoint pode derrubar dezenas de serviços dependentes. Diferentes estratégias de versionamento oferecem graus variados de controle:
- Versionamento por URL:
/api/v1/usersvs/api/v2/users— simples e explícito, mas polui a estrutura de rotas. - Versionamento por header:
Accept: application/vnd.api.v1+json— mais elegante, mas exige configuração extra no cliente. - Versionamento por media type: combina header e formato de resposta — flexível, porém complexo.
No PHP, a escolha depende do contexto: APIs públicas geralmente preferem URL ou header, enquanto APIs internas podem usar abordagens mais leves.
2. Estratégias de Versionamento no Laravel
Versionamento via Prefixo de Rota
A abordagem mais direta utiliza grupos de rotas com prefixos versionados:
// routes/api.php
Route::prefix('v1')->group(function () {
Route::get('/users', [V1\UserController::class, 'index']);
});
Route::prefix('v2')->group(function () {
Route::get('/users', [V2\UserController::class, 'index']);
});
Cada versão tem seu próprio controlador, permitindo evolução independente. A desvantagem é a duplicação de código se as versões compartilham lógica.
Versionamento via Cabeçalhos HTTP
Usando middlewares para interpretar headers personalizados:
// app/Http/Middleware/APIVersion.php
public function handle($request, Closure $next)
{
$accept = $request->header('Accept');
if (str_contains($accept, 'application/vnd.api.v2+json')) {
$request->attributes->set('api_version', 'v2');
} else {
$request->attributes->set('api_version', 'v1');
}
return $next($request);
}
Em seguida, o controlador pode decidir qual lógica executar baseado na versão:
public function index(Request $request)
{
$version = $request->attributes->get('api_version', 'v1');
return $version === 'v2'
? $this->v2Response()
: $this->v1Response();
}
Versionamento via Parâmetros de Query
/api/users?version=2 — simples de implementar, mas quebra o princípio RESTful e pode ser facilmente ignorado por clientes. Além disso, URLs com parâmetros não são cacheáveis eficientemente.
Route::get('/users', function (Request $request) {
$version = $request->query('version', '1');
// Lógica condicional baseada na versão
});
Limitações principais: poluição de URLs, dificuldade de versionamento de headers, e propensão a erros do lado do cliente.
3. Implementando Backwards Compatibility na Prática
Adicionar Novos Endpoints sem Modificar os Existentes
A regra de ouro: nunca remova ou altere a assinatura de endpoints existentes. Em vez disso, adicione novos:
// V1 - endpoint original
Route::get('/v1/users', [V1\UserController::class, 'index']);
// V2 - novo endpoint com dados adicionais
Route::get('/v2/users', [V2\UserController::class, 'index']);
Se precisar estender um endpoint existente, use parâmetros opcionais:
// Em V1, o campo 'email' não existia
public function index(Request $request)
{
$users = User::all();
if ($request->attributes->get('api_version') === 'v2') {
return UserResource::collection($users);
}
return V1UserResource::collection($users);
}
Utilizar Transformers e Presenters
Com o pacote league/fractal ou spatie/laravel-fractal, você pode criar transformadores específicos por versão:
use League\Fractal\TransformerAbstract;
class UserV1Transformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
];
}
}
class UserV2Transformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email, // Novo campo
'profile_url' => route('profile', $user),
];
}
}
Estratégias de Depreciação
Headers Deprecation e Sunset informam clientes sobre versões obsoletas:
return response()
->json($data)
->header('Deprecation', 'true')
->header('Sunset', 'Sat, 01 Jan 2025 00:00:00 GMT');
No Laravel, um middleware pode adicionar esses headers automaticamente:
public function handle($request, Closure $next)
{
$response = $next($request);
if ($request->attributes->get('api_version') === 'v1') {
$response->header('Deprecation', 'true');
$response->header('Sunset', '2025-01-01');
}
return $response;
}
4. Versionamento com Laravel e Ferramentas Auxiliares
Grupos de Rotas e Middlewares
Organize versões com middlewares que resolvem a versão automaticamente:
// routes/api_v1.php
Route::middleware('api.version:v1')->group(function () {
Route::apiResource('users', V1\UserController::class);
});
// routes/api_v2.php
Route::middleware('api.version:v2')->group(function () {
Route::apiResource('users', V2\UserController::class);
});
VersionResolver Customizado
Crie uma classe que decide a versão baseada em múltiplos fatores:
class VersionResolver
{
public function resolve(Request $request): string
{
// Prioridade: header > URL > query parameter
if ($header = $request->header('X-API-Version')) {
return $header;
}
if (preg_match('/\/v(\d+)\//', $request->path(), $matches)) {
return 'v' . $matches[1];
}
return $request->query('version', 'v1');
}
}
Integração com dingo/api
O pacote dingo/api oferece versionamento automático com roteamento inteligente:
$api = app('Dingo\Api\Routing\Router');
$api->version('v1', function ($api) {
$api->get('users', 'App\Api\V1\Controllers\UserController@index');
});
$api->version('v2', function ($api) {
$api->get('users', 'App\Api\V2\Controllers\UserController@index');
});
Ele também suporta transformadores e respostas formatadas automaticamente.
5. Evolução de Schemas e Migrações de Dados
Mudanças de Campos entre Versões
Quando um campo é removido ou renomeado, versões antigas ainda precisam dele:
// Migration que adiciona campo, mas preserva o antigo
Schema::table('users', function (Blueprint $table) {
$table->string('full_name')->nullable(); // Novo campo
// Campo 'name' permanece para V1
});
Transformers para Campos Obsoletos
class UserV1Transformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'name' => $user->name, // Campo legado
'email' => $user->email,
];
}
}
class UserV2Transformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'full_name' => $user->full_name, // Novo campo
'email' => $user->email,
];
}
}
API Resources do Laravel
Use API Resources para controlar a saída por versão:
class UserResource extends JsonResource
{
public function toArray($request)
{
$version = $request->attributes->get('api_version', 'v1');
$data = [
'id' => $this->id,
'email' => $this->email,
];
if ($version === 'v1') {
$data['name'] = $this->name;
} else {
$data['full_name'] = $this->full_name;
}
return $data;
}
}
6. Testando a Compatibilidade Reversa
Testes de Integração para Versões Antigas
class APIVersioningTest extends TestCase
{
public function test_v1_users_endpoint_returns_legacy_format()
{
$response = $this->getJson('/api/v1/users');
$response->assertStatus(200);
$response->assertJsonStructure([
'data' => [
'*' => ['id', 'name', 'email']
]
]);
}
public function test_v2_users_endpoint_returns_new_format()
{
$response = $this->getJson('/api/v2/users');
$response->assertStatus(200);
$response->assertJsonStructure([
'data' => [
'*' => ['id', 'full_name', 'email', 'profile_url']
]
]);
}
}
Automatização em CI/CD
No phpunit.xml, configure testes específicos para versões:
<testsuites>
<testsuite name="API V1">
<directory>tests/Feature/API/V1</directory>
</testsuite>
<testsuite name="API V2">
<directory>tests/Feature/API/V2</directory>
</testsuite>
</testsuites>
Em pipelines CI, execute ambos os suites para garantir que versões antigas não quebraram.
7. Monitoramento e Estratégias de Descontinuação
Logging de Chamadas a Versões Antigas
// No middleware de versão
public function handle($request, Closure $next)
{
$version = $this->resolver->resolve($request);
if ($version === 'v1') {
Log::channel('deprecated_api')->info('Chamada a V1', [
'path' => $request->path(),
'user_agent' => $request->userAgent(),
'ip' => $request->ip(),
]);
}
return $next($request);
}
Cronogramas de Depreciação
- Anúncio: Comunique a descontinuação com 6 meses de antecedência.
- Headers de aviso: Adicione
DeprecationeSunset3 meses antes. - Redirecionamento: 1 mês antes, redirecione V1 para V2 com aviso.
- Remoção: Após a data
Sunset, remova a rota e retorne 410 Gone.
Remoção Segura com Fallbacks
Route::fallback(function () {
return response()->json([
'error' => 'API version deprecated',
'message' => 'Please migrate to /api/v2',
'documentation' => 'https://docs.api.com/migration'
], 410);
});
Referências
- Laravel API Versioning Best Practices — Documentação oficial do Laravel sobre versionamento de APIs.
- Dingo API Package — Pacote popular para construção de APIs RESTful no Laravel com suporte a versionamento automático.
- API Versioning with Headers in PHP — Artigo técnico da PHP Architect sobre estratégias de versionamento por headers.
- Backwards Compatibility in PHP APIs — Guia prático no Dev.to sobre manutenção de compatibilidade reversa.
- HTTP Sunset Header Specification — Draft do IETF sobre headers de depreciação e sunset para APIs.
- Fractal Transformers for PHP — Documentação do League\Fractal para criação de transformadores de API.
- Semantic Versioning for APIs — Especificação oficial de versionamento semântico, aplicável a APIs REST.