Como versionar APIs sem quebrar clientes

1. Fundamentos do Versionamento de APIs

Versionar APIs é uma prática essencial para permitir que sistemas evoluam sem prejudicar consumidores existentes. Quando uma API muda seu comportamento, formato de resposta ou requisitos de entrada, clientes que dependem da versão anterior podem falhar inesperadamente. O custo de não versionar inclui clientes quebrados, retrabalho emergencial e perda de confiança na plataforma.

O princípio fundamental é a compatibilidade reversa (backward compatibility): mudanças não devem quebrar funcionalidades existentes. Quando mudanças incompatíveis são inevitáveis, o versionamento permite que clientes antigos continuem funcionando enquanto novos adotam a versão atualizada. A depreciação gradual, com avisos e prazos claros, completa o ciclo.

2. Estratégias de Versionamento por URI

O versionamento na URL é a abordagem mais direta:

GET /api/v1/usuarios/123
GET /api/v2/usuarios/123

Vantagens:
- Simplicidade: qualquer desenvolvedor entende imediatamente
- Descoberta explícita: a versão está visível na chamada
- Cache independente: cada versão tem sua própria URL

Desvantagens:
- Poluição de URLs: cada endpoint precisa de múltiplas rotas
- Duplicação de código: controllers e serviços podem se repetir
- Dificuldade de evolução incremental: mudanças pequenas exigem nova versão na URL

Exemplo de implementação em Node.js:

// Rota para v1
app.get('/api/v1/usuarios/:id', (req, res) => {
  const usuario = { id: req.params.id, nome: 'João', email: 'joao@email.com' };
  res.json(usuario);
});

// Rota para v2 (adiciona campo telefone)
app.get('/api/v2/usuarios/:id', (req, res) => {
  const usuario = { id: req.params.id, nome: 'João', email: 'joao@email.com', telefone: '11999999999' };
  res.json(usuario);
});

3. Versionamento por Cabeçalhos HTTP

O versionamento por cabeçalhos oferece mais flexibilidade:

GET /api/usuarios/123
Accept: application/vnd.api.v2+json

Ou usando cabeçalho customizado:

GET /api/usuarios/123
X-API-Version: 2

Vantagens:
- URLs limpas e estáveis
- Fácil de evoluir sem mudar endpoints
- Suporte nativo em frameworks modernos

Desvantagens:
- Complexidade: clientes precisam configurar cabeçalhos corretamente
- Menos descoberta: a versão não é óbvia na URL
- Cache: pode exigir configuração adicional para Vary headers

Exemplo de implementação:

app.get('/api/usuarios/:id', (req, res) => {
  const version = req.headers['accept']?.includes('v2') ? 2 : 1;

  if (version === 1) {
    res.json({ id: req.params.id, nome: 'João', email: 'joao@email.com' });
  } else {
    res.json({ id: req.params.id, nome: 'João', email: 'joao@email.com', telefone: '11999999999' });
  }
});

4. Versionamento por Parâmetros de Query

Esta abordagem usa parâmetros na URL:

GET /api/usuarios/123?version=2

Quando usar: APIs simples, internas ou de baixo tráfego.

Vantagens:
- Fácil de implementar e testar
- Não requer configuração de cabeçalhos

Limitações:
- Cache: URLs com parâmetros diferentes podem não ser cacheadas corretamente
- Legibilidade: URLs longas com múltiplos parâmetros
- Segurança: parâmetros podem ser esquecidos ou mal interpretados

Exemplo:

app.get('/api/usuarios/:id', (req, res) => {
  const version = req.query.version || '1';

  if (version === '1') {
    res.json({ id: req.params.id, nome: 'João', email: 'joao@email.com' });
  } else {
    res.json({ id: req.params.id, nome: 'João', email: 'joao@email.com', telefone: '11999999999' });
  }
});

5. Práticas para Transições Suaves

Para evitar quebrar clientes durante mudanças:

Depreciação com aviso antecipado:
Use cabeçalhos Warning ou Sunset para informar clientes:

HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Warning: 299 - "API version 1 will be deprecated on 2024-12-31"

Manter versões antigas ativas:
Defina um período mínimo de suporte (ex.: 6 meses após o anúncio de depreciação).

Estratégia de sunset:
- Anuncie a data de descontinuação
- Ofereça guias de migração
- Monitore o tráfego das versões antigas

6. Testes e Garantia de Compatibilidade

Testes de contrato com Pact:

// Pact test para garantir compatibilidade
pact.addInteraction({
  state: 'usuário existe',
  uponReceiving: 'requisição para v1',
  withRequest: { method: 'GET', path: '/api/v1/usuarios/123' },
  willRespondWith: { status: 200, body: { id: '123', nome: 'João' } }
});

Monitoramento de chamadas:
Implemente logs que identifiquem a versão usada por cada cliente:

// Middleware de logging de versão
app.use((req, res, next) => {
  const version = req.path.match(/\/api\/(v\d+)\//)?.[1] || 'unknown';
  console.log(`[${new Date().toISOString()}] Versão: ${version} - Cliente: ${req.ip}`);
  next();
});

Feature flags para liberação gradual:
Use flags para ativar novas versões para um subconjunto de clientes.

7. Exemplos Práticos e Boas Práticas

Combinação de estratégias (URI + cabeçalho):

// Versão na URI para roteamento principal
// Cabeçalho Accept para detalhes específicos

app.get('/api/v2/usuarios/:id', (req, res) => {
  const formato = req.headers['accept']?.includes('detalhado') ? 'detalhado' : 'simples';

  const usuario = {
    id: req.params.id,
    nome: 'João',
    email: 'joao@email.com',
    telefone: '11999999999'
  };

  if (formato === 'detalhado') {
    usuario.endereco = 'Rua A, 123';
    usuario.dataCriacao = '2023-01-01';
  }

  res.json(usuario);
});

Documentação clara com changelogs:

# Changelog da API v2.1.0
## Novidades
- Adicionado campo `telefone` no endpoint /usuarios/:id
- Novo endpoint /usuarios/:id/historico

## Mudanças quebram compatibilidade?
- NÃO: v2.1.0 é compatível com v2.0.0
- SIM: v2.0.0 não é compatível com v1.x

## Depreciações
- v1 será descontinuada em 31/12/2024
- Migre para v2 até essa data

Ferramentas de versionamento automático:
Use OpenAPI para documentar versões:

openapi: 3.0.0
info:
  title: API de Usuários
  version: 2.0.0
  description: |
    ## Histórico de versões
    - v1 (depreciada): endpoint básico
    - v2 (atual): inclui telefone e endereço

paths:
  /api/v2/usuarios/{id}:
    get:
      summary: Retorna dados do usuário
      parameters:
        - name: id
          in: path
          required: true
      responses:
        '200':
          description: Sucesso
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsuarioV2'

Referências