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.