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 interfaceDocumentaçã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
Conteúdos relacionados