Versionamento de APIs REST em Node.js

APIs evoluem, rotas mudam, campos são renomeados, comportamentos são corrigidos. Sem versionamento, cada mudança quebra clientes existentes. Com uma estratégia de versionamento clara desde o início, você pode evoluir a API sem breaking changes e dar tempo para os clientes migrarem.

URI versioning: /v1, /v2 com Express Router

A abordagem mais comum e explícita:

typescript
// Estrutura de arquivos:
// src/
//   routes/
//     v1/
//       pedidos.ts    → GET /api/v1/pedidos
//       usuarios.ts   → GET /api/v1/usuarios
//     v2/
//       pedidos.ts    → GET /api/v2/pedidos (nova versão)
//       usuarios.ts   → GET /api/v2/usuarios

// src/routes/v1/pedidos.ts:
import { Router } from 'express'
const router = Router()

router.get('/', async (req, res) => {
  const pedidos = await db.query('SELECT id, status, total FROM pedidos WHERE usuario_id = $1', [req.usuario!.userId])
  // v1: campo "total" como número
  res.json({ pedidos: pedidos.rows })
})

export default router

// src/routes/v2/pedidos.ts:
import { Router } from 'express'
const router = Router()

router.get('/', async (req, res) => {
  const pedidos = await db.query('SELECT id, status, valor_total, moeda FROM pedidos WHERE usuario_id = $1', [req.usuario!.userId])
  // v2: campo "valor_total" + "moeda" separados, novo endpoint de filtros
  res.json({
    pedidos: pedidos.rows,
    _meta: { versao: 'v2', deprecacoes: [] },
  })
})

export default router

// src/app.ts — registrar versões:
import v1Pedidos from './routes/v1/pedidos'
import v2Pedidos from './routes/v2/pedidos'
import v1Usuarios from './routes/v1/usuarios'
import v2Usuarios from './routes/v2/usuarios'

// Middleware de autenticação aplicado a todas as versões:
app.use('/api', autenticar)

app.use('/api/v1/pedidos', v1Pedidos)
app.use('/api/v2/pedidos', v2Pedidos)
app.use('/api/v1/usuarios', v1Usuarios)
app.use('/api/v2/usuarios', v2Usuarios)

// Alias sem versão aponta para a mais recente:
app.use('/api/pedidos', v2Pedidos)

Middleware de versionamento por header

Versionar via Accept ou Api-Version header (mais flexível para clientes):

typescript
// Header versioning: Accept: application/vnd.minha-api.v2+json
// ou: Api-Version: 2024-01-01 (formato de data — usado por Stripe, GitHub)

// Middleware de extração de versão:
export function extrairVersao(req: Request, res: Response, next: NextFunction) {
  // Suportar múltiplos formatos:
  const versao =
    // /api/v2/pedidos → versão da URL
    req.path.match(/^/v(\d+)\//)?.[1] ||
    // Api-Version: 2024-06-01
    req.headers['api-version'] as string ||
    // Accept: application/vnd.minha-api.v2+json
    req.headers.accept?.match(/vnd\.minha-api\.v(\d+)/)?.[1] ||
    '1'  // default: versão 1

  req.apiVersion = versao

  // Adicionar header de versão na resposta:
  res.setHeader('Api-Version', versao)
  res.setHeader('Api-Deprecated-Versions', '1')

  next()
}

app.use(extrairVersao)

// Handler que despacha por versão:
app.get('/api/pedidos', autenticar, async (req, res) => {
  switch (req.apiVersion) {
    case '2':
      return await getPedidosV2(req, res)
    case '1':
    default:
      return await getPedidosV1(req, res)
  }
})

// Para APIs que evoluem muito: estratégia de transformação
// (um único handler, transformar a resposta por versão):
async function getPedidos(req: Request, res: Response) {
  const dados = await pedidosService.listar(req.usuario!.userId)

  const resposta = req.apiVersion === '2'
    ? transformarParaV2(dados)
    : transformarParaV1(dados)

  res.json(resposta)
}

Deprecar versões com headers e avisos

Comunicar descontinuação para clientes antes de remover:

typescript
// middleware/deprecacao.ts
const VERSOES_DEPRECADAS: Record<string, { sunset: string; successorUrl: string }> = {
  'v1': {
    sunset: '2025-12-31',
    successorUrl: 'https://api.seudominio.com.br/api/v2',
  },
}

// Middleware que adiciona headers de deprecação quando versão antiga é usada:
export function avisoDeprecacao(req: Request, res: Response, next: NextFunction) {
  const versaoUri = req.path.match(/^\/v(\d+)\//)?.[1]
  if (!versaoUri) return next()

  const info = VERSOES_DEPRECADAS[`v${versaoUri}`]
  if (!info) return next()

  // Headers padronizados de deprecação (RFC 8594):
  res.setHeader('Deprecation', 'true')
  res.setHeader('Sunset', new Date(info.sunset).toUTCString())
  res.setHeader('Link', `<${info.successorUrl}>; rel="successor-version"`)

  // Aviso opcional no corpo (para clientes que não leem headers):
  // Mas cuidado: não quebrar o formato da resposta

  next()
}

app.use('/api/v1', avisoDeprecacao)

// Log de uso de versões deprecadas para monitoramento:
app.use('/api/v1', (req, res, next) => {
  logger.warn({
    tipo: 'versao_deprecada',
    versao: 'v1',
    path: req.path,
    clienteId: req.usuario?.userId,
    userAgent: req.headers['user-agent'],
  })
  next()
})

Versionamento de rotas individuais com fastify-plugin

Versionar apenas as rotas que mudaram (não toda a API):

typescript
// Padrão: só versionar rotas que tiveram breaking changes
// Rotas sem breaking change: mesma implementação nas duas versões

// Util para registrar rota em múltiplas versões:
function registrarVersoes(
  app: Express,
  path: string,
  versoes: Record<string, Router>
) {
  Object.entries(versoes).forEach(([versao, router]) => {
    app.use(`/api/${versao}${path}`, router)
  })
}

// Pedidos teve breaking change entre v1 e v2:
registrarVersoes(app, '/pedidos', {
  v1: routerPedidosV1,
  v2: routerPedidosV2,
})

// Usuarios não teve breaking change — usar o mesmo router:
registrarVersoes(app, '/usuarios', {
  v1: routerUsuarios,
  v2: routerUsuarios,  // mesma implementação
})

// Checklist para decidir se precisa de nova versão:
// ❌ Breaking changes (exigem nova versão):
//    - Remover campo da resposta
//    - Renomear campo da resposta
//    - Mudar tipo de campo (string → number)
//    - Mudar semântica de parâmetro
//    - Remover endpoint

// ✅ Non-breaking (não precisam de nova versão):
//    - Adicionar campo opcional à resposta
//    - Adicionar endpoint novo
//    - Adicionar parâmetro opcional
//    - Melhorar performance sem mudar interface

Documentação de changelog por versão

Documentar o que mudou entre versões:

typescript
// GET /api/changelog — endpoint de changelog programático:
app.get('/api/changelog', (req, res) => {
  res.json({
    versaoAtual: 'v2',
    versoes: {
      v2: {
        lancamento: '2024-06-01',
        status: 'estavel',
        mudancas: [
          {
            tipo: 'breaking',
            descricao: 'Campo "total" renomeado para "valor_total" em pedidos',
            endpoint: 'GET /pedidos',
          },
          {
            tipo: 'adicao',
            descricao: 'Novo campo "moeda" em pedidos',
            endpoint: 'GET /pedidos',
          },
          {
            tipo: 'adicao',
            descricao: 'Novo endpoint de filtro avançado',
            endpoint: 'GET /pedidos/filtrar',
          },
        ],
      },
      v1: {
        lancamento: '2023-01-01',
        status: 'deprecado',
        sunset: '2025-12-31',
        migracaoGuia: 'https://docs.seudominio.com.br/migracao-v1-v2',
      },
    },
  })
})

Não quer configurar manualmente?

Implante o VPS para APIs em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

URI versioning (/api/v1/pedidos): explícito, fácil de testar no browser, fácil de logar e rotear no Nginx, funciona sem configuração especial. Header versioning (Api-Version: 2024-01-01): URL não muda entre versões, formato de data permite granularidade maior, usado por Stripe e GitHub APIs. Para a maioria das APIs: URI versioning é mais simples e menos propenso a erros de implementação. Para APIs de grande escala com muitos clientes SDK: header versioning com data (estilo Stripe) escala melhor.

Depende da sua base de clientes: para APIs públicas com muitos integradores: mínimo 12 meses após o anúncio de deprecação. Para APIs internas com equipes controladas: 3-6 meses. Para APIs de produtos SaaS com contratos: alinhar com o ciclo de contrato. Comunique a deprecação o mais cedo possível, documente a migração, e monitore quantos clientes ainda usam a versão antiga antes de remover.

Separar em camadas: a lógica de negócio (services) não é versionada, os handlers de rota (controllers) fazem a transformação por versão. Um único método pedidosService.listar() retorna os dados; transformarParaV1() e transformarParaV2() convertem o formato. Assim, bugs corrigidos no service beneficiam todas as versões automaticamente.

Sunset (RFC 8594) é a data em que o endpoint será desligado definitivamente. O header Sunset: Mon, 31 Dec 2025 23:59:59 GMT informa o cliente quando a versão parará de funcionar. Ferramentas como Postman e bibliotecas SDK leem esse header para alertar desenvolvedores. É uma convenção padronizada, use junto com Deprecation: true e Link para o successor.

Adicionar /v1 desde o início tem custo zero e dá flexibilidade futura, vá de /api/pedidos para /api/v1/pedidos antes de qualquer cliente externo integrar. Para APIs internas usadas apenas por equipe controlada: pode começar sem versão e adicionar quando necessário. Para APIs públicas, SDKs e integrações de terceiros: versione desde o primeiro endpoint público para evitar a dor de migrar clientes existentes depois.