← voltar ao índice

· 11 min de leitura · por Tiago

OpenAPI na prática: documentando APIs .NET

Se você já trabalhou em uma API que precisou ser consumida por outro time, por um aplicativo mobile ou por um parceiro externo, provavelmente já esbarrou na pergunta "onde está a documentação disso?". É exatamente esse problema que o OpenAPI tenta resolver de forma definitiva, transformando a documentação de um artefato manual e desatualizado em um contrato formal, legível por máquina, que pode alimentar desde uma interface interativa até a geração automática de clientes em várias linguagens.

Neste post vamos entender o que é o OpenAPI, por que ele se tornou o padrão de facto para descrever APIs REST, como gerá-lo e mantê-lo em projetos .NET (com exemplos que também se aplicam a outros ecossistemas) e quais são as armadilhas mais comuns que fazem a especificação virar só mais um arquivo esquecido no repositório.

O que é o OpenAPI e por que ele existe

O OpenAPI Specification (OAS) é um formato padronizado, geralmente escrito em YAML ou JSON, para descrever a interface de uma API HTTP: quais rotas existem, quais métodos aceitam, quais parâmetros e corpos de requisição são esperados, quais respostas podem ser retornadas e com qual formato, além de detalhes como esquemas de autenticação. Ele nasceu do projeto Swagger, doado posteriormente para a Linux Foundation, e hoje é mantido pela OpenAPI Initiative.

A razão de sua popularidade não é burocrática, é prática. Quando a interface de uma API é descrita em um formato estruturado e não apenas em prosa dentro de um wiki, ela deixa de ser apenas "documentação" e passa a ser um contrato que pode ser processado por ferramentas. Isso abre portas para automações valiosas: gerar uma interface interativa de testes (Swagger UI, Scalar, Redoc), gerar clientes tipados em C#, TypeScript, Java ou Python automaticamente, validar se a implementação real da API está de acordo com o que foi prometido, e até usar a especificação como insumo para testes de contrato entre serviços.

Em outras palavras, o OpenAPI resolve o problema clássico de documentação que fica desatualizada: se a especificação é gerada a partir do código, ou se o código é validado contra a especificação, a distância entre "o que está documentado" e "o que realmente acontece" tende a zero.

Anatomia de um documento OpenAPI

Antes de partir para o código, vale entender a estrutura básica de um documento OpenAPI, porque isso ajuda a interpretar o que as ferramentas geram automaticamente. Um documento mínimo tem essa forma:

openapi: 3.1.0
info:
  title: API de Pedidos
  version: "1.0.0"
paths:
  /pedidos/{id}:
    get:
      summary: Retorna um pedido pelo identificador
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Pedido encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pedido"
        "404":
          description: Pedido não encontrado
components:
  schemas:
    Pedido:
      type: object
      required: [id, status, total]
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pendente, pago, cancelado]
        total:
          type: number
          format: double

Repare em três blocos importantes. O paths descreve as rotas e operações disponíveis, incluindo parâmetros e respostas possíveis para cada código HTTP. O components/schemas centraliza a definição dos tipos de dados, permitindo reaproveitá-los via $ref em várias respostas, o que evita duplicação e mantém a especificação consistente. E o campo required dentro de um schema é crucial: ele diz explicitamente quais campos sempre estarão presentes, algo que muita gente esquece de declarar e que gera ambiguidade para quem consome a API.

Entender essa estrutura importa mesmo quando você não escreve o YAML manualmente, porque quando algo sai errado na geração automática (um schema não aparece, um enum não é refletido corretamente), você precisa saber onde procurar.

Gerando OpenAPI em projetos ASP.NET Core

A partir do .NET 9, o ASP.NET Core passou a ter suporte nativo à geração de documentos OpenAPI, sem depender de uma biblioteca de terceiros para o core da funcionalidade (embora Swagger UI ainda venha de outro pacote para a parte visual). Isso é uma mudança significativa porque reduz a superfície de dependências no projeto.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/pedidos/{id:guid}", (Guid id) =>
{
    var pedido = new Pedido(id, "pendente", 259.90m);
    return Results.Ok(pedido);
})
.WithName("ObterPedido")
.WithSummary("Retorna um pedido pelo identificador");

app.Run();

record Pedido(Guid Id, string Status, decimal Total);

Aqui, AddOpenApi() registra os serviços que inspecionam os endpoints mapeados e produzem o documento em tempo de execução, e MapOpenApi() expõe esse documento (por padrão em /openapi/v1.json). O ponto importante é que a geração é feita a partir do próprio código: os tipos de retorno, os parâmetros de rota e os atributos como WithSummary viram metadados no JSON gerado. Isso é a abordagem "code-first": você escreve os endpoints normalmente e o OpenAPI é um subproduto, sempre sincronizado com a implementação.

Se o projeto ainda estiver em versões anteriores ao .NET 9, ou se você precisar de recursos mais avançados de customização (como anotações detalhadas de segurança, agrupamento por tags customizadas ou suporte a versionamento mais sofisticado), o pacote Swashbuckle continua sendo uma escolha sólida e amplamente usada em produção:

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "API de Pedidos",
        Version = "v1"
    });

    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "Informe o token JWT no formato: Bearer {token}"
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty<string>()
        }
    });
});

app.UseSwagger();
app.UseSwaggerUI();

Esse trecho mostra algo que costuma ser negligenciado: descrever o esquema de segurança. Sem o bloco AddSecurityDefinition e AddSecurityRequirement, a especificação até descreve as rotas, mas não informa que elas exigem autenticação, o que engana clientes que tentam integrar sem saber que precisam enviar um token. Isso é um detalhe pequeno no código, mas com grande impacto para quem consome a API às cegas, apenas lendo a especificação.

Code-first versus design-first

Existem duas filosofias para produzir uma especificação OpenAPI, e escolher a errada para o seu contexto gera atrito desnecessário. Na abordagem code-first, que os exemplos acima ilustram, você escreve o código da API normalmente e a especificação é derivada automaticamente dele. A vantagem é a sincronia automática: é praticamente impossível a documentação divergir da implementação, porque uma gera a outra. A desvantagem é que o design da API acaba sendo guiado pelas convenções do framework e pela forma como os tipos foram modelados no código, o que nem sempre resulta na interface mais amigável para quem consome.

Na abordagem design-first (ou contract-first), você escreve o YAML da especificação antes de qualquer linha de código de implementação, geralmente com ferramentas como Stoplight Studio ou editores com plugins de validação, e depois gera esqueletos de controllers ou usa a especificação para revisão e aprovação entre times antes de codificar. Essa abordagem é especialmente valiosa quando múltiplos times (ou até empresas parceiras) precisam concordar sobre o formato da API antes que qualquer código seja escrito, porque o contrato se torna o ponto central de negociação, evitando retrabalho depois que a implementação já está pronta.

Na prática, times pequenos e produtos internos costumam se beneficiar mais do code-first, pela velocidade e pela garantia de sincronia. Já equipes de plataforma que expõem APIs públicas, com contratos de estabilidade e SLA, tendem a preferir design-first, porque o custo de quebrar um contrato depois de publicado é alto e a revisão prévia da especificação evita esse problema.

Consumindo a especificação: geração de clientes

Uma vez que você tem um documento OpenAPI confiável, uma das aplicações mais úteis é gerar clientes tipados automaticamente, eliminando a necessidade de escrever manualmente chamadas HTTP e classes de DTO em cada aplicação consumidora. Com o NSwag, por exemplo, é possível gerar um client C# a partir do JSON exposto pela própria API:

dotnet tool install -g NSwag.ConsoleCore

nswag openapi2csclient \
  /input:https://minhaapi.com/openapi/v1.json \
  /classname:PedidosClient \
  /namespace:MinhaEmpresa.Clientes \
  /output:PedidosClient.cs

O client gerado inclui métodos fortemente tipados para cada operação, classes para cada schema definido em components/schemas e tratamento de erros baseado nos códigos de resposta documentados. Isso é particularmente valioso em arquiteturas de microsserviços internos, onde um time consome a API de outro: em vez de duplicar manualmente a modelagem dos DTOs (e correr o risco de ficarem dessincronizados), o client é regenerado sempre que a especificação muda, e qualquer breaking change aparece como erro de compilação no lado consumidor, não como um bug descoberto em produção.

Vale notar que essa geração automática só é confiável se a especificação for precisa. Se os schemas estiverem incompletos, com campos opcionais marcados como obrigatórios ou vice-versa, o client gerado vai propagar esse erro, e a "proteção" da tipagem estática se transforma em uma falsa sensação de segurança.

Armadilhas comuns

Um erro frequente é tratar o campo required do schema de forma descuidada. Em C#, um record com uma propriedade string? Observacao pode ser interpretado pela ferramenta de geração como opcional automaticamente, mas isso depende de como o nullable reference types está configurado no projeto e de como a biblioteca de geração interpreta isso. É importante revisar o JSON gerado manualmente pelo menos uma vez, comparando com a intenção real da API, porque divergências aqui geram bugs sutis nos clientes gerados, como campos que deveriam ser obrigatórios sendo tratados como opcionais.

Outra armadilha comum é a falta de exemplos (examples) nos schemas e operações. Um schema tecnicamente correto, mas sem nenhum exemplo de payload, obriga quem está integrando a inferir valores válidos, o que é especialmente doloroso para campos como enums, formatos de data ou strings com padrões específicos (como CPF ou CNPJ). Adicionar exemplos realistas custa pouco e economiza um tempo considerável de suporte e dúvidas no time consumidor.

Versionamento também costuma gerar confusão. Muitos projetos mantêm um único documento OpenAPI que tenta descrever todas as versões da API simultaneamente, misturando rotas antigas e novas sob a mesma especificação, o que confunde tanto humanos quanto ferramentas de geração de clientes. O caminho mais são é manter um documento por versão principal da API (por exemplo, /openapi/v1.json e /openapi/v2.json), cada um representando fielmente o estado daquela versão específica.

Por fim, um erro relacionado a segurança: expor o Swagger UI ou o endpoint do documento OpenAPI em produção sem nenhuma restrição de acesso. Embora a documentação em si não seja necessariamente um segredo, ela revela detalhes de implementação, formatos internos de erro e às vezes até rotas administrativas que não deveriam ser descobertas por qualquer pessoa. É comum restringir o MapOpenApi() e o UseSwaggerUI() para ambientes de desenvolvimento e homologação, ou protegê-los atrás de autenticação quando precisam existir em produção.

Validando a especificação com linting

Assim como código-fonte se beneficia de um linter, uma especificação OpenAPI também se beneficia de validação automática de estilo e consistência. Ferramentas como o Spectral permitem definir regras (por exemplo, "toda operação deve ter um summary", "todo schema de resposta 4xx deve seguir um formato padrão de erro") e rodar essa validação como parte do pipeline de CI, falhando o build se a especificação não seguir as convenções da organização. Isso é especialmente valioso em empresas com múltiplas APIs, onde a consistência entre elas facilita a vida de quem consome mais de um serviço.

# .spectral.yaml
extends: spectral:oas
rules:
  operation-summary-formatted:
    description: Toda operação deve ter um summary descritivo
    given: "$.paths[*][*]"
    severity: error
    then:
      field: summary
      function: truthy

Rodar esse tipo de verificação automaticamente evita que a especificação vá se degradando lentamente ao longo do tempo, à medida que diferentes desenvolvedores adicionam endpoints sem seguir os mesmos padrões.

Guia rápido de decisão

Se sua API é interna, consumida majoritariamente por times próximos e o ritmo de mudança é alto, prefira a abordagem code-first com o suporte nativo do ASP.NET Core ou Swashbuckle, priorizando velocidade e sincronia automática. Se sua API é pública, tem contrato de estabilidade com clientes externos ou parceiros, ou exige aprovação formal de design antes da implementação, prefira design-first, escrevendo a especificação antes do código e validando com linting no CI. Se você precisa integrar múltiplos serviços internos escritos em linguagens diferentes, invista em geração automática de clientes a partir do OpenAPI (com NSwag, openapi-generator ou ferramentas equivalentes), porque isso elimina uma classe inteira de bugs de integração manual. E independentemente do cenário, trate a especificação como um artefato de primeira classe no repositório, com revisão em pull requests e validação automatizada, exatamente como você trataria testes ou infraestrutura como código.

Conclusão

O OpenAPI deixou de ser apenas "aquela tela do Swagger" para se tornar a espinha dorsal de como equipes descrevem, validam e integram APIs HTTP de forma confiável. O valor real não está em ter um YAML bonito guardado em algum lugar, mas em usar esse documento como fonte de verdade que alimenta ferramentas: interfaces interativas para desenvolvedores explorarem a API, geração automática de clientes tipados que eliminam integração manual, e validação de consistência que evita a degradação silenciosa da qualidade ao longo do tempo. No ecossistema .NET, o suporte nativo trazido pelo ASP.NET Core moderno tornou essa prática ainda mais acessível, mas o benefício central independe da linguagem: uma especificação bem cuidada economiza tempo de integração, reduz mal-entendidos entre times e transforma a documentação de um fardo em um ativo do produto.

← voltar ao índice