Versionamento de APIs REST
Um microsserviço é implantado no seu próprio ritmo, mas os consumidores dele não: um app móvel que os usuários nunca atualizam, um parceiro que integrou uma vez, o serviço de outro time, com os seus próprios prazos. Quando a API precisa mudar de um jeito que os quebraria, o provedor precisa de uma forma de oferecer o contrato novo ao lado do antigo, e de uma forma de aposentar o antigo com ordem. Isso é versionamento de API.
Esta página responde:
- Quando uma mudança precisa de uma nova versão, e quando não precisa?
- Onde vai a versão: no caminho, num parâmetro de query, num header ou no media type?
- Como implementar no Spring, com o suporte nativo do Spring Boot 4 e do jeito clássico?
- Como depreciar e aposentar a versão antiga sem surpreender ninguém?
Ela complementa a seção Evoluindo um contrato da aula de 2027.1 sobre estilos de API. O GraphQL segue outro caminho, com um único schema que evolui por @deprecated: veja REST vs GraphQL.
Quando você precisa de uma nova versão?
Só para mudanças incompatíveis (breaking changes): aquelas depois das quais um consumidor existente, sem alteração, para de funcionar. Todo o resto é uma mudança compatível e sai na versão atual.
| Mudança | Incompatível? | Por quê |
|---|---|---|
| adicionar um endpoint novo | não | ninguém o chama ainda |
| adicionar um campo numa resposta | não, para leitores tolerantes | os clientes ignoram campos que não conhecem |
| adicionar um campo de requisição ou parâmetro de query opcional | não | as requisições antigas continuam válidas |
| remover ou renomear um campo da resposta | sim | quem o lia recebe null ou falha |
mudar o tipo, o formato ou a unidade de um campo (price: 10.5 → "10.50", reais → centavos) | sim | o valor é lido ou interpretado errado |
| tornar um campo da requisição obrigatório, ou endurecer a validação | sim | requisições antigas passam a receber 400 |
| devolver um valor novo de enum | muitas vezes | um cliente com um switch sobre os valores conhecidos quebra |
mudar um código de status (200 → 202) ou o formato de erro | sim | a lógica do cliente depende deles |
| mudar a ordenação padrão ou o tamanho de página | sutilmente | clientes que dependiam disso mostram dados errados |
| remover um endpoint | sim | as chamadas recebem 404 |
Dois hábitos tornam a maioria das mudanças compatível e evitam a maioria das versões novas:
- Leitor tolerante (tolerant reader)7: o consumidor lê só os campos de que precisa e ignora o resto. O mapeador JSON do Spring Boot ignora propriedades desconhecidas por padrão (o Jackson 3, usado pelo Spring Boot 4, tornou isso o padrão da biblioteca), então mantenha assim.
- Expandir e contrair (parallel change)8: em vez de renomear
price, adicionepriceCentsao lado, migre os consumidores e só removapricequando ninguém mais o ler. Um campo também faz parte do contrato, e pode ser depreciado sozinho.
Quando uma mudança incompatível é inevitável, aumente a versão major (versionamento semântico9: MAJOR.MINOR.PATCH). Mudanças compatíveis não precisam de uma versão que o cliente tenha de pedir.
Quatro formas de levar a versão
| Estratégia | Requisição | Vantagens | Desvantagens |
|---|---|---|---|
| Caminho da URI | GET /api/v2/orders/7 | visível em links, logs e no navegador; fácil de rotear no gateway ou no ingress; cacheável pela URL | o "mesmo" recurso ganha duas URLs; costuma versionar a API inteira de uma vez |
| Parâmetro de query | GET /api/orders/7?version=2 | fácil de adicionar; pode ser opcional, com um padrão | fácil de esquecer; mistura-se com filtros; os caches precisam considerar a query string |
| Header próprio | GET /api/orders/7 + API-Version: 2 | URLs limpas e estáveis; pode ser definido uma vez no cliente | invisível nos links; caches e CDNs precisam de Vary: API-Version; mais difícil de testar à mão |
| Media type | Accept: application/vnd.example+json;version=2 | versiona a representação por negociação de conteúdo, o mais fiel ao REST | o mais difícil de documentar, testar e rotear; os caches precisam de Vary: Accept |
Não existe um padrão. O caminho da URI é a escolha mais comum em APIs públicas, porque é o mais fácil de ver, rotear e depurar. Um header é comum em APIs internas cujos clientes são todos código. Seja qual for a escolha, use uma única estratégia em todos os seus serviços e documente-a na descrição OpenAPI.
Versionamento no Spring
Spring Framework 7 e Spring Boot 4: suporte nativo
O Spring Framework 7.0 (novembro de 2025) e o Spring Boot 4.0 trouxeram versionamento de API nativo para o Spring MVC2. Ele tem três partes:
- Resolver a versão de cada requisição, com uma das quatro estratégias.
- Interpretar e validar a versão. O parser padrão lê
MAJOR.MINOR.PATCHe pula umvinicial, entãov2é o mesmo que2.0.0. Uma versão que não é suportada recebe400 Bad Request. - Mapear a requisição para um método de controller com o novo atributo
versionde@RequestMapping(e de@GetMapping,@PostMapping…).
A configuração mais simples usa propriedades do Spring Boot3:
spring:
mvc:
apiversion:
use:
header: API-Version
default: "1.0"
supported: ["1.0", "2.0"]
O mesmo em Java, com ApiVersionConfigurer:
@Configuration
public class ApiVersionConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useRequestHeader("API-Version")
.addSupportedVersions("1.0", "2.0")
.setDefaultVersion("1.0");
}
}
Por padrão, uma requisição sem versão recebe 400. Configurar uma versão padrão torna a versão opcional: as requisições que não enviam nenhuma recebem a padrão.
Só os métodos que mudaram precisam de versão. Um mapeamento sem version atende a qualquer versão, mas qualquer método versionado que também atenda tem prioridade sobre ele. Uma versão de base (baseline) como "2.0+" atende à 2.0 e a toda versão suportada acima dela, então o método segue valendo na 2.1, na 2.2… até que um mais novo o substitua:
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping(path = "/{id}", version = "1.0")
public OrderV1 getV1(@PathVariable Long id) {
return OrderV1.from(service.find(id)); // price in reais, as a decimal
}
@GetMapping(path = "/{id}", version = "2.0+")
public OrderV2 getV2(@PathVariable Long id) {
return OrderV2.from(service.find(id)); // priceCents + currency
}
@PostMapping // unchanged: serves every version
public ResponseEntity<Void> create(@RequestBody NewOrder order) {
return ResponseEntity.created(service.create(order)).build();
}
}
As outras estratégias só trocam o resolver:
| Estratégia | ApiVersionConfigurer | Propriedade do Spring Boot |
|---|---|---|
| caminho da URI | usePathSegment(1): o índice do segmento em /api/{version}/orders, que os mapeamentos precisam declarar como variável de URI | spring.mvc.apiversion.use.path-segment=1 |
| parâmetro de query | useQueryParam("version") | spring.mvc.apiversion.use.query-parameter=version |
| header | useRequestHeader("API-Version") | spring.mvc.apiversion.use.header=API-Version |
| media type | useMediaTypeParameter(MediaType.parseMediaType("application/vnd.example+json"), "version") | spring.mvc.apiversion.use.media-type-parameter (um mapa de media type para nome de parâmetro) |
Os clientes ganham o mesmo suporte. Um serviço que chama outro define a versão uma vez no seu RestClient (também no WebClient e nos clientes de interface HTTP):
RestClient orders = RestClient.builder()
.baseUrl("http://order:8080")
.apiVersionInserter(ApiVersionInserter.useHeader("API-Version"))
.defaultApiVersion("2.0")
.build();
Versões anteriores: mapeamentos comuns
Antes do Spring Framework 7 (Spring Boot 3.x e anteriores), não existe o atributo version. Cada estratégia é escrita com os atributos que o @RequestMapping sempre teve1:
@RestController
@RequestMapping("/api/v1/orders")
public class OrderControllerV1 {
@GetMapping("/{id}")
public OrderV1 get(@PathVariable Long id) { ... }
}
@RestController
@RequestMapping("/api/v2/orders")
public class OrderControllerV2 {
@GetMapping("/{id}")
public OrderV2 get(@PathVariable Long id) { ... }
}
Funciona, mas você mesmo precisa tratar uma versão ausente ou desconhecida (dependendo da estratégia, termina num 400, 404 ou 406), e todo método versionado repete a condição. Nos dois estilos, mantenha a regra de negócio num único service e deixe só os controllers e os DTOs mudarem por versão.
Numa arquitetura de microsserviços, a versão também pode escolher um deployment: o gateway ou o ingress do Kubernetes manda /api/v1/** para os pods da release antiga e /api/v2/** para a nova. Cada versão então escala, e um dia é desligada, de forma independente.
Depreciação e sunset
Uma versão é aposentada em etapas, e o cliente deveria ficar sabendo de cada etapa pela própria API, não só por e-mail. Dois headers de resposta padronizados levam essa informação:
Deprecation(RFC 9745, março de 2025)4: este recurso está (ou estará) depreciado a partir desta data. Ele ainda funciona, mas você deveria migrar. O valor é uma data de structured field:@seguido dos segundos Unix. Rascunhos anteriores usavamDeprecation: true, e você ainda pode encontrá-lo.Sunset(RFC 8594, maio de 2019)5: a partir desta data, espera-se que o recurso pare de responder. O valor é uma data HTTP. A RFC 9745 exige que o sunset não seja anterior à depreciação.- Headers
Linkcomrel="deprecation"erel="sunset"apontam para a documentação para humanos: o guia de migração, a política de aposentadoria.
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: @1795996800
Sunset: Mon, 31 May 2027 00:00:00 GMT
Link: <https://api.example.com/docs/migrate-to-v2>; rel="deprecation"; type="text/html"
Aqui @1795996800 é 30 de novembro de 2026, 00:00 UTC. O ciclo de vida da v1, passo a passo:
sequenceDiagram
participant C as Cliente
participant G as API gateway
participant V1 as orders v1
participant V2 as orders v2
Note over V1: 1. v1 no ar
C->>G: GET /api/v1/orders/7
G->>V1: rota /api/v1/**
V1-->>C: 200 OK
Note over V2: 2. v2 lançada, v1 ainda atendida
Note over V1: 3. v1 depreciada
C->>G: GET /api/v1/orders/7
G->>V1: rota /api/v1/**
V1-->>C: 200 OK + headers Deprecation, Sunset e Link
C->>G: GET /api/v2/orders/7 (o cliente migra)
G->>V2: rota /api/v2/**
V2-->>C: 200 OK
Note over V1: 4. data do sunset: o tráfego da v1 deve estar perto de zero
Note over G: 5. v1 removida
C->>G: GET /api/v1/orders/7 (um cliente esquecido)
G-->>C: 410 Gone + Link para o guia de migração - Meça antes de remover. Conte as requisições por versão e por cliente, por exemplo com um rótulo
versionnas métricas de requisição. Remova a v1 quando o tráfego dela chegar a zero, ou quando os clientes restantes tiverem sido avisados pessoalmente, e não só porque o calendário mandou. - Responda com um erro claro depois da remoção.
410 Gonecom um link para o guia de migração explica mais que um404seco. A RFC 8594 não define um código: depois do sunset, vale de um erro a um redirecionamento. - Publique uma política, por exemplo "uma versão major é suportada por pelo menos 6 meses depois que a sucessora sai", para que os consumidores possam se planejar.
No Spring Framework 7, o StandardApiVersionDeprecationHandler escreve esses headers em toda requisição a uma versão depreciada2:
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
StandardApiVersionDeprecationHandler deprecation = new StandardApiVersionDeprecationHandler();
deprecation.configureVersion("1.0")
.setDeprecationDate(ZonedDateTime.parse("2026-11-30T00:00:00Z"))
.setDeprecationLink(URI.create("https://api.example.com/docs/migrate-to-v2"))
.setSunsetDate(ZonedDateTime.parse("2027-05-31T00:00:00Z"));
configurer.useRequestHeader("API-Version")
.addSupportedVersions("1.0", "2.0")
.setDefaultVersion("1.0")
.setDeprecationHandler(deprecation);
}
Em versões anteriores, defina os mesmos headers você mesmo, nos controllers depreciados ou uma vez num filtro para todo o /api/v1/**:
@GetMapping("/{id}")
public ResponseEntity<OrderV1> get(@PathVariable Long id) {
return ResponseEntity.ok()
.header("Deprecation", "@1795996800")
.header("Sunset", "Mon, 31 May 2027 00:00:00 GMT")
.header(HttpHeaders.LINK, "<https://api.example.com/docs/migrate-to-v2>; rel=\"deprecation\"; type=\"text/html\"")
.body(OrderV1.from(service.find(id)));
}
Pegando quebras antes da produção: testes de contrato
Como saber que uma mudança é compatível antes de implantá-la? Com contratos orientados ao consumidor (consumer-driven contracts), cada consumidor publica as interações de que depende, por exemplo "order chama GET /api/v1/products/{id} e lê name e price". O CI do provedor executa todos os contratos publicados contra o novo build e falha se algum quebrar. Uma mudança proibida aparece então como um build vermelho, e não como um incidente em produção, e os contratos ainda dizem exatamente quais consumidores ainda usam a v1. As ferramentas comuns são Pact e Spring Cloud Contract6.
Pontos-chave
- Crie uma nova versão só para mudanças incompatíveis: remover ou renomear campos, mudar tipos ou significado, novas entradas obrigatórias, códigos de status diferentes. Mudanças aditivas saem na versão atual.
- Leitores tolerantes e expandir e contrair mantêm a maioria das mudanças compatível, então a maioria delas não precisa de versão nova.
- A versão pode ir no caminho da URI, num parâmetro de query, num header próprio ou no media type. O caminho é o mais visível e o mais fácil de rotear; escolha uma estratégia para todos os serviços.
- O Spring Framework 7 / Spring Boot 4 tem versionamento nativo:
ApiVersionConfigurerouspring.mvc.apiversion.*para resolver a versão, e@GetMapping(version = "2.0+")para mapeá-la. Versões anteriores usamparams,headers,producesou caminhos separados. - Aposente uma versão em etapas: anuncie com os headers
Deprecation(RFC 9745) eSunset(RFC 8594) e umLinkpara o guia de migração, meça o tráfego por versão e remova quando o tráfego acabar. - Testes de contrato orientados ao consumidor (Pact, Spring Cloud Contract) pegam mudanças incompatíveis no CI.
-
Jacky, Versioning RESTful APIs with Spring Boot: A Step-by-Step Guide in 5 minutes. ↩
-
Spring Framework: API Versioning, com a configuração do MVC e o atributo
versiondos mapeamentos. ↩↩ -
RFC 9745: The Deprecation HTTP Response Header Field. IETF, 2025. ↩
-
RFC 8594: The Sunset HTTP Header Field. IETF, 2019. ↩
-
FOWLER, M., Tolerant Reader. ↩
-
SATO, D., Parallel Change. ↩