Estratégias de versionamento de contrato em APIs GraphQL

1. Fundamentos do versionamento em APIs GraphQL

No ecossistema REST, o versionamento tradicionalmente é feito através de URLs (/v1/, /v2/) ou headers customizados (Accept: application/vnd.api.v1+json). Essas abordagens criam múltiplas superfícies de API que precisam ser mantidas simultaneamente, aumentando a complexidade operacional.

GraphQL introduz uma mudança fundamental: o cliente especifica exatamente quais dados precisa, e o servidor responde apenas com esses dados. O contrato da API é definido pelo schema — um documento que descreve todos os tipos, campos e argumentos disponíveis. Esse contrato é fortemente tipado e auto-documentado.

A abordagem ideal no GraphQL não é criar múltiplas versões da API, mas sim evoluir o schema existente de forma que mudanças sejam sempre compatíveis com versões anteriores. Versões de URL ou header são consideradas antipadrão porque fragmentam o ecossistema de clientes e dificultam a manutenção.

2. Versionamento evolutivo com deprecação de campos

A estratégia mais fundamental é usar a diretiva @deprecated nativa do GraphQL. Ela permite marcar campos que devem ser removidos no futuro, dando tempo para os consumidores migrarem.

type User {
  id: ID!
  name: String!
  username: String @deprecated(reason: "Use 'name' instead. Removed after 2025-06-01")
  email: String!
  oldEmail: String @deprecated(reason: "Use 'email' field. Will be removed in next major version")
}

A migração gradual pode ser feita em três fases:

  1. Anúncio: Marcar campo como @deprecated com razão clara
  2. Convivência: Manter ambos os campos por um período definido
  3. Remoção: Após monitoramento, remover o campo obsoleto

Para monitorar o uso de campos obsoletos, é possível analisar queries reais:

# Exemplo de query que ainda usa campo deprecated
query GetUser {
  user(id: "123") {
    name
    username  # campo deprecated
  }
}

Ferramentas como Apollo Studio permitem rastrear quais campos estão sendo consultados, ajudando a decidir quando é seguro remover um campo.

3. Estratégia de extensão sem quebra (Backward-Compatible)

A regra de ouro do versionamento GraphQL é: nunca remova, apenas adicione. Toda mudança deve ser adicionada de forma que clientes antigos continuem funcionando.

Exemplo de adição segura de novo campo opcional:

type Product {
  id: ID!
  name: String!
  price: Float!
  # Novo campo opcional adicionado na versão 2.0
  discountPrice: Float
  # Novo argumento com valor default
  description(language: String = "en"): String!
}

Clientes podem usar aliases para compatibilidade:

# Cliente antigo continua funcionando
query GetProduct {
  product(id: "456") {
    name
    price
  }
}

# Cliente novo usa o novo campo
query GetProduct {
  product(id: "456") {
    name
    price
    discountPrice
  }
}

Fragments também ajudam na transição:

fragment ProductV1 on Product {
  name
  price
}

fragment ProductV2 on Product {
  ...ProductV1
  discountPrice
}

4. Versionamento por camada de schema (Schema Stitching)

Para cenários onde mudanças significativas são necessárias, o schema stitching permite criar subschemas independentes que são combinados em um gateway.

# Schema v1 (gateway principal)
type Query {
  user(id: ID!): UserV1
}

# Schema v2 (subschema separado)
type Query {
  user(id: ID!): UserV2
}

O gateway pode rotear queries baseado em regras:

# Configuração de roteamento no gateway
const gatewayConfig = {
  schemas: [
    { name: "v1", schema: schemaV1, matcher: (query) => query.includes("UserV1") },
    { name: "v2", schema: schemaV2, matcher: (query) => true }
  ]
}

Com Apollo Federation, o versionamento distribuído funciona naturalmente:

# Serviço de usuários versão 1
extend type Query {
  userV1(id: ID!): UserV1 @external
}

# Serviço de usuários versão 2
extend type Query {
  userV2(id: ID!): UserV2 @external
}

5. Versionamento por diretivas personalizadas

Diretivas customizadas oferecem controle granular sobre versões de campos e argumentos.

# Definição da diretiva @version
directive @version(from: String!, until: String) on FIELD_DEFINITION | ARGUMENT_DEFINITION

# Uso no schema
type User {
  id: ID!
  name: String!
  email: String! @version(from: "1.0.0")
  phone: String @version(from: "2.0.0", until: "3.0.0")
}

Implementação de middleware que filtra campos por versão:

# Middleware que processa a diretiva @version
function versionMiddleware(resolve, parent, args, context, info) {
  const clientVersion = context.headers["x-api-version"] || "1.0.0"
  const fieldVersion = getVersionFromDirective(info.fieldDefinition)

  if (fieldVersion && !isVersionCompatible(clientVersion, fieldVersion)) {
    return null  # Campo não disponível para esta versão
  }

  return resolve(parent, args, context, info)
}

6. Estratégias de quebra controlada (Breaking Changes)

Mesmo com boas práticas, algumas mudanças quebram o contrato. As principais são:

  • Remoção de campo: Remove um campo que clientes podem estar usando
  • Tornar campo não-nulo: Quebra clientes que não esperam o campo
  • Alterar tipo de argumento: Incompatibilidade de tipos

Para gerenciar quebras, use sunset headers:

# Resposta HTTP com sunset header
HTTP/1.1 200 OK
Sunset: Sat, 01 Jun 2025 23:59:59 GMT
Deprecation: true
Link: <https://api.example.com/schema/v2>; rel="successor-version"

Processo de comunicação recomendado:

  1. Anúncio: Comunicar a mudança com 6 meses de antecedência
  2. Transição: Manter ambos os comportamentos por 3 meses
  3. Remoção: Remover após período de transição

Changelog automático com schema diff:

# Comando para gerar diff entre schemas
graphql-inspector diff schema-v1.graphql schema-v2.graphql
# Saída: "Field 'User.oldEmail' was removed"
# Saída: "Field 'User.email' changed type from String to String!"

7. Ferramentas e boas práticas para gerenciamento de contrato

graphql-inspector é a ferramenta mais importante para validação de mudanças:

# Verificar se mudanças são compatíveis
graphql-inspector validate schema-v1.graphql schema-v2.graphql --rules ./rules.yml

# Regras de validação personalizadas
rules:
  - no-breaking-changes: true
  - no-schema-removal: true
  - require-deprecation-reason: true

Testes de contrato com schema snapshot:

# Jest test para snapshot do schema
import { buildSchema } from 'graphql'

test('schema snapshot', () => {
  const schema = buildSchema(schemaSDL)
  expect(printSchema(schema)).toMatchSnapshot()
})

Integração contínua com CI/CD:

# GitHub Actions para validar schema
name: Schema Validation
on: pull_request
steps:
  - uses: actions/checkout@v3
  - run: npx graphql-inspector validate ./schema/*.graphql
  - run: npx graphql-inspector diff ./schema/production.graphql ./schema/pr.graphql

Versionamento de schema com Git:

# Estrutura de diretórios recomendada
schemas/
  v1/
    user.graphql
    product.graphql
  v2/
    user.graphql
    product.graphql
  changelog.md

Revisão de pull requests deve incluir checklist específico:

  • [ ] Mudança é backward-compatible?
  • [ ] Campos removidos estão deprecated há pelo menos 3 meses?
  • [ ] Novos campos têm valores default?
  • [ ] Documentação foi atualizada?
  • [ ] Changelog foi atualizado?

Conclusão

O versionamento de contrato em APIs GraphQL não precisa ser complexo. A chave está em adotar uma mentalidade evolutiva: adicionar em vez de remover, deprecar em vez de deletar, e comunicar mudanças com transparência. Ferramentas como graphql-inspector, testes de contrato e CI/CD garantem que mudanças sejam seguras e rastreáveis.

Lembre-se: no GraphQL, o contrato é seu schema. Cuide dele como cuidaria de qualquer contrato legal — com cláusulas claras, períodos de transição e documentação adequada.

Referências