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',
          },
        },
      })
    })

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes