← Voltar para o blog

OpenAPI em Go: Contratos com oapi-codegen

Aprenda a usar OpenAPI em Go com oapi-codegen para criar APIs contract-first, tipos gerados, validação, mocks, CI e evolução segura de contratos.

OpenAPI em Go é uma resposta prática para um problema que quase todo time backend encontra quando a API deixa de ser pequena: como manter contrato, implementação, documentação, testes e clientes falando a mesma língua? Em muitos projetos, a documentação começa atualizada no Swagger, mas o handler muda, o frontend adapta por tentativa e erro, o client mobile quebra em silêncio e ninguém sabe qual campo é obrigatório de verdade. O resultado é retrabalho, integração lenta e bugs que só aparecem quando outro time consome a API.

Go combina bem com APIs explícitas porque valoriza tipos, simplicidade e contratos pequenos. Mas só escrever handlers com net/http, Chi, Echo ou Gin não garante que o contrato público esteja sincronizado. O arquivo OpenAPI descreve rotas, parâmetros, payloads, códigos de resposta, schemas e exemplos. Ferramentas como oapi-codegen usam essa especificação para gerar tipos Go, interfaces de servidor e clientes, reduzindo a distância entre documentação e código.

Este guia mostra como usar OpenAPI em Go em um fluxo contract-first saudável: escrever a especificação, gerar tipos, implementar handlers, validar requests, testar compatibilidade e evoluir a API sem quebrar consumidores. Ele complementa os guias de API REST em Go, autenticação e autorização em APIs Go, Testcontainers em Go e feature flags para rollout seguro.

Quando OpenAPI vale a pena

OpenAPI não é obrigatório para todo projeto Go. Se você está criando uma API interna pequena, com um único consumidor e pouca mudança, um README claro e testes de integração podem bastar. O ganho aparece quando o contrato passa a ser compartilhado:

  1. Frontend web, mobile, parceiros ou outros serviços consomem a API.
  2. Mais de um time altera rotas e payloads.
  3. A API precisa de documentação pública ou semipública.
  4. Clientes SDK precisam ser gerados ou mantidos.
  5. Mudanças de contrato precisam passar por review antes do deploy.
  6. Integrações precisam de mocks para desenvolvimento paralelo.

Em vagas Go backend no Brasil, OpenAPI e Swagger aparecem com frequência junto de AWS, Kubernetes, autenticação, mensageria, observabilidade e CI/CD. Saber explicar esse fluxo mostra maturidade porque conecta código com colaboração entre times, não apenas com sintaxe de handler.

Contract-first ou code-first?

Existem duas formas comuns de trabalhar. No modelo code-first, você escreve handlers e anotações no código, depois gera Swagger/OpenAPI a partir disso. É simples para começar e funciona em times menores. O risco é a documentação virar um subproduto do código, sem review real de contrato.

No modelo contract-first, você escreve ou altera o arquivo openapi.yaml antes da implementação. O contrato vira o ponto de alinhamento entre backend, frontend, QA, produto e consumidores externos. Só depois você gera os tipos Go e implementa o comportamento. Para APIs com consumidores reais, esse fluxo costuma ser mais seguro.

Um contrato mínimo pode começar assim:

openapi: 3.0.3
info:
  title: Orders API
  version: 1.0.0
paths:
  /orders/{id}:
    get:
      operationId: getOrder
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Pedido encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "404":
          description: Pedido não encontrado
components:
  schemas:
    Order:
      type: object
      required: [id, status, total_cents]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [pending, paid, canceled]
        total_cents:
          type: integer
          format: int64

O detalhe importante é operationId. Ele dá nome estável para o método gerado. Se você deixa a ferramenta inferir nomes a partir da rota, refactors pequenos podem gerar diffs grandes no código.

Gerando código com oapi-codegen

oapi-codegen é uma das ferramentas mais usadas no ecossistema Go para gerar tipos, interfaces de servidor e clientes a partir de OpenAPI. Instale uma versão fixada no projeto, não uma versão solta na máquina de cada pessoa. Por exemplo, use a versão v2.4.1 registrada em tools.go, go:generate ou no script de build:

go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen

Em projetos de produção, prefira declarar o comando em tools.go ou usar go:generate com versão documentada. Um layout simples:

api/
  openapi.yaml
internal/api/
  generated.gen.go
  handlers.go

O comando para gerar tipos e interface de servidor pode ser:

oapi-codegen \
  -generate types,chi-server,spec \
  -package api \
  -o internal/api/generated.gen.go \
  api/openapi.yaml

Se você usa Echo, Gin ou servidor mais manual, ajuste o gerador. A ideia é manter o arquivo gerado separado do código escrito à mão. Nunca edite generated.gen.go manualmente; altere o contrato ou a configuração de geração.

Implementando handlers tipados

Com a interface gerada, seu código implementa métodos com assinatura conhecida. Um exemplo simplificado com Chi:

package api

import (
    "encoding/json"
    "net/http"
)

type Server struct {
    orders OrderRepository
}

func (s *Server) GetOrder(w http.ResponseWriter, r *http.Request, id string) {
    order, err := s.orders.Find(r.Context(), id)
    if err == ErrNotFound {
        http.Error(w, "pedido não encontrado", http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, "erro interno", http.StatusInternalServerError)
        return
    }

    response := Order{
        Id:         order.ID,
        Status:     OrderStatus(order.Status),
        TotalCents: order.TotalCents,
    }

    w.Header().Set("Content-Type", "application/json")
    _ = json.NewEncoder(w).Encode(response)
}

O ganho está na fronteira. Se o contrato exige total_cents como int64, você tem um tipo gerado representando isso. Se o status vira enum, o código precisa lidar com valores válidos. Se um campo muda de nome, o build denuncia onde o handler ainda usa a estrutura antiga.

Isso não elimina testes. Ele reduz uma classe de divergências entre contrato e implementação, mas regras de negócio, permissões, comportamento de erro e integração com banco continuam precisando de testes automatizados.

Validação de requests

Um erro comum é gerar tipos e esquecer validação em runtime. Tipos Go ajudam o código interno, mas requests HTTP chegam como bytes. Se você não valida rota, query string, headers e body contra o OpenAPI, ainda pode aceitar payload inválido e responder algo diferente do contrato.

Use middleware de validação com a especificação embutida. O fluxo típico é carregar o spec gerado, configurar validação e registrar as rotas:

swagger, err := GetSwagger()
if err != nil {
    return err
}

swagger.Servers = nil // evita conflito entre server URL do spec e ambiente local

r := chi.NewRouter()
r.Use(OapiRequestValidator(swagger))
HandlerFromMux(server, r)

Em produção, cuide da resposta de erro. O validador pode retornar mensagens técnicas demais para usuário final. Padronize erros em JSON, registre o motivo em log estruturado e exponha uma mensagem segura. Combine isso com slog em Go para investigar payloads inválidos sem vazar dados sensíveis.

Testes de contrato

OpenAPI fica forte quando entra no CI. O mínimo saudável é validar que o arquivo está bem formado, que o código gerado está atualizado e que handlers respondem conforme o contrato.

Um script simples pode falhar o build quando alguém altera openapi.yaml mas esquece de regenerar:

go generate ./...
git diff --exit-code -- internal/api/generated.gen.go
go test ./...

Para endpoints críticos, escreva testes HTTP em cima do router real:

func TestGetOrderContract(t *testing.T) {
    router := newTestRouter(t)

    req := httptest.NewRequest(http.MethodGet, "/orders/ord_123", nil)
    rec := httptest.NewRecorder()

    router.ServeHTTP(rec, req)

    if rec.Code != http.StatusOK {
        t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
    }

    var got api.Order
    if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
        t.Fatalf("decode response: %v", err)
    }
    if got.Id == "" || got.TotalCents <= 0 {
        t.Fatalf("invalid contract response: %+v", got)
    }
}

Quando o endpoint depende de PostgreSQL, Redis ou fila, combine esse teste com Testcontainers em Go. Assim você valida o contrato HTTP e a integração real por trás dele.

Evoluindo contratos sem quebrar clientes

OpenAPI não impede breaking changes por mágica. Ele torna a mudança visível. Algumas regras ajudam:

  1. Adicionar campo opcional geralmente é compatível.
  2. Remover campo, renomear propriedade ou mudar tipo é breaking change.
  3. Tornar campo opcional em obrigatório quebra clientes existentes.
  4. Trocar código de resposta sem compatibilidade quebra automações.
  5. Reusar enum sem pensar em valores antigos causa bugs silenciosos.

Para APIs públicas ou consumidas por muitos times, versionamento explícito pode ser necessário: /v1, /v2, headers de versão ou compatibilidade por feature flag. Para APIs internas, às vezes basta janela de migração, comunicação clara e métrica de uso. O importante é não tratar o arquivo OpenAPI como decoração; ele precisa participar do review.

Ferramentas de diff de OpenAPI ajudam a detectar breaking changes no CI. Você pode comparar main contra o branch atual e falhar quando o contrato remove campo ou altera tipo sem aprovação. Esse tipo de gate combina bem com pipelines descritos em GoReleaser, checksums e SBOM: artefato confiável começa antes do binário, no contrato que ele promete servir.

Mocks e desenvolvimento paralelo

Um benefício subestimado de OpenAPI é permitir trabalho paralelo. Frontend pode usar mock server a partir do contrato enquanto backend implementa. QA pode preparar casos de teste antes de a rota estar pronta. Times parceiros podem revisar payloads sem esperar deploy em staging.

Isso exige disciplina nos exemplos. Um schema correto sem exemplo realista ajuda pouco. Inclua exemplos de sucesso e erro, especialmente para validação, autenticação, paginação e estados de negócio. Em APIs brasileiras de pagamentos, logística, educação ou fintech, mensagens de erro e campos monetários precisam ser claros. Use centavos inteiros para dinheiro, documente timezone, e evite float para valores financeiros.

Armadilhas comuns

A primeira armadilha é gerar código demais. Se você gera servidor, client, models, validação e mocks sem entender o fluxo, o projeto vira refém da ferramenta. Comece com tipos e interface de servidor; adicione clientes gerados quando houver consumidor Go real.

A segunda é deixar o contrato genérico demais. map[string]interface{} no schema vira perda de tipo no Go. Use schemas explícitos, enums quando o domínio é fechado e objetos separados para request e response quando necessário.

A terceira é expor detalhe interno. OpenAPI descreve contrato público, não tabela do banco. Não copie nomes de coluna, IDs internos ou campos de auditoria só porque estão no model. Crie DTOs de API e faça o mapeamento no handler ou camada de aplicação.

A quarta é ignorar autenticação. Documente securitySchemes, escopos, headers esperados e erros 401/403. Depois conecte isso ao guia de autenticação em APIs Go para não deixar autorização como comentário solto.

A quinta é esquecer carreira. Muitas vagas Go backend citam APIs REST, Swagger/OpenAPI, AWS, Docker, Kubernetes e CI/CD. Saber explicar contrato, geração, validação e compatibilidade mostra que você entende integração entre times. Para comparar como esse tema aparece em outras stacks brasileiras, acompanhe também o portal Python Dev Brasil reúne vagas Python e dados no Brasil, onde APIs, dados e automação também exigem contratos claros.

Checklist de produção

Antes de tratar OpenAPI como pronto, valide:

  1. openapi.yaml passa em lint e review.
  2. operationId é estável e legível.
  3. Código gerado é reproduzível no CI.
  4. generated.gen.go não é editado manualmente.
  5. Middleware valida request contra o spec.
  6. Erros de validação são padronizados e seguros.
  7. Testes HTTP cobrem respostas principais.
  8. Breaking changes são detectados antes do merge.
  9. Exemplos de request e response são realistas.
  10. Autenticação, paginação e códigos de erro estão documentados.

OpenAPI em Go não substitui bom design de API. Ele torna o design verificável. Quando o contrato é revisado, gerado, validado e testado, a API deixa de depender da memória do time e passa a ter uma fonte de verdade que o compilador e o CI conseguem cobrar.