Para documentar uma API Axum em Rust com OpenAPI, use utoipa para gerar o contrato a partir dos tipos e, se quiser exploração interativa, sirva o Swagger UI com utoipa-swagger-ui. Na prática: derive ToSchema nas structs de entrada e saída, anote os handlers com #[utoipa::path], monte um #[derive(OpenApi)] com as tags e paths da API e exponha /api-docs/openapi.json junto com a UI. Assim a documentação deixa de ser um Markdown esquecido e passa a acompanhar o código que realmente compila.
Esse padrão responde à pergunta que aparece em portfólios, code reviews e vagas Rust: “onde está o contrato da API?”. Times que já usam Axum, Serde, SQLx e Tower ganham onboarding mais rápido, clientes tipados e uma forma clara de discutir breaking changes. Se você ainda está montando a base HTTP, comece pelo tutorial de API REST com Axum e pelo guia de serviços resilientes com Tower e Axum.
Resposta rápida: quando vale documentar com utoipa?
| Situação | Recomendação |
|---|---|
| API interna consumida por um único frontend no mesmo repo | OpenAPI ainda ajuda, mas pode começar enxuto |
| API pública, BFF ou integração com outros times | Documente cedo com utoipa |
| Portfólio para vaga backend/plataforma | Inclua OpenAPI + exemplos de erro |
| Microsserviço gRPC | Prefira .proto; use OpenAPI só nas bordas REST |
| Protótipo de um fim de semana | Adie a UI; mantenha ao menos schemas básicos |
| Contrato com versionamento e clientes externos | Trate o JSON OpenAPI como artefato de release |
A decisão não é “Swagger para tudo”. É escolher uma fonte de verdade próxima do compilador para request, response, status codes e autenticação.
Por que OpenAPI importa em APIs Rust
Rust já oferece muita segurança de tipos dentro do processo. O buraco aparece na fronteira HTTP: status codes, headers, formatos de erro, paginação e autenticação precisam ser comunicados a pessoas e a outras stacks. OpenAPI (também conhecido pelo legado “Swagger”) descreve esse contrato em JSON ou YAML de forma independente de linguagem.
Em times brasileiros de fintech, logística, healthtech e plataformas B2B, a API costuma ser consumida por frontend TypeScript, mobile, parceiros e jobs internos. Sem contrato explícito, cada mudança vira conversa de Slack e regressão silenciosa. Com contrato versionado, o review passa a discutir:
- campos obrigatórios versus opcionais;
- enums estáveis;
- erros previsíveis;
- autenticação Bearer ou API key;
- compatibilidade entre versões.
Isso conversa diretamente com carreira. Em entrevistas de backend Rust e no nicho web/backend, saber explicar como o time evita drift entre código e documentação pesa tanto quanto citar o framework.
O que é utoipa
utoipa gera documentação OpenAPI a partir de anotações e derives Rust. Em vez de manter um arquivo YAML paralelo, você descreve schemas com #[derive(ToSchema)], documenta handlers com #[utoipa::path(...)] e agrega tudo em um tipo OpenApi. O resultado é um documento que pode ser servido como JSON, renderizado no Swagger UI ou consumido por geradores de cliente.
Pontos fortes para APIs Axum:
- schemas reutilizam as mesmas structs Serde da aplicação;
- paths ficam perto dos handlers;
- tags organizam recursos (
users,orders,health); - security schemes documentam JWT e API keys;
- a UI facilita exploração local sem Postman no primeiro dia.
Limitações honestas:
- macros aumentam a superfície de aprendizado;
- handlers muito dinâmicos exigem cuidado na modelagem;
- a documentação não substitui testes nem observabilidade;
- upgrades de Axum/utoipa pedem leitura das notas de versão.
Uma alternativa citada no ecossistema é Aide, com outra ergonomia de extratores e roteamento. Este guia aprofunda utoipa porque ele é uma resposta direta e pesquisável para “OpenAPI no Axum” e encaixa bem no estilo derive-first que a maioria dos times Rust já usa com Serde.
Dependências de partida
Considere um serviço Axum 0.8 alinhado aos exemplos do site:
[dependencies]
axum = { version = "0.8", features = ["macros"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
utoipa = { version = "5", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "8", features = ["axum"] }
tower-http = { version = "0.6", features = ["trace", "cors"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["serde", "v4"] }
thiserror = "2"
Fixe versões no lockfile do projeto real e confira os features exatos da release que você adotar. A linha axum_extras ajuda a mapear extratores comuns do Axum para a documentação. Se a API autentica usuários, você vai acrescentar crates de JWT depois; o contrato OpenAPI já pode declarar o security scheme desde o início.
Modele os schemas com ToSchema
Comece pelas structs que já existem na borda HTTP. O ideal é que request, response e erro compartilhem as mesmas definições usadas pelos handlers.
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct CriarTarefaRequest {
/// Título curto exibido na lista
#[schema(example = "Estudar utoipa")]
pub titulo: String,
/// Detalhe opcional da tarefa
pub descricao: Option<String>,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct TarefaResponse {
pub id: Uuid,
pub titulo: String,
pub descricao: Option<String>,
pub concluida: bool,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct ErroApi {
/// Código estável para o cliente tratar o caso
pub codigo: String,
/// Mensagem legível para humanos
pub mensagem: String,
}
Boas práticas de schema:
- documente campos com comentários
///— eles viram description no OpenAPI; - use
Optionde propósito; não transforme tudo em string vazia; - prefira enums Serde para status em vez de strings soltas;
- evite expor structs de persistência internas se elas carregam colunas sensíveis;
- dê exemplos realistas; a UI fica muito mais útil.
Se a API pagina resultados, modele explicitamente items, next_cursor e limit. Contratos vagos de “objeto livre” empurram o problema para o consumidor.
Documente handlers com utoipa::path
O próximo passo é amarrar método HTTP, path, parâmetros, body e respostas ao handler.
use axum::{extract::Path, http::StatusCode, Json};
use uuid::Uuid;
/// Cria uma nova tarefa
#[utoipa::path(
post,
path = "/tarefas",
tag = "tarefas",
request_body = CriarTarefaRequest,
responses(
(status = 201, description = "Tarefa criada", body = TarefaResponse),
(status = 400, description = "Payload inválido", body = ErroApi),
(status = 401, description = "Não autenticado", body = ErroApi)
),
security(
("bearer_auth" = [])
)
)]
pub async fn criar_tarefa(
Json(payload): Json<CriarTarefaRequest>,
) -> Result<(StatusCode, Json<TarefaResponse>), (StatusCode, Json<ErroApi>)> {
if payload.titulo.trim().is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json(ErroApi {
codigo: "titulo_obrigatorio".into(),
mensagem: "Informe um título".into(),
}),
));
}
Ok((
StatusCode::CREATED,
Json(TarefaResponse {
id: Uuid::new_v4(),
titulo: payload.titulo,
descricao: payload.descricao,
concluida: false,
}),
))
}
/// Busca uma tarefa pelo id
#[utoipa::path(
get,
path = "/tarefas/{id}",
tag = "tarefas",
params(
("id" = Uuid, Path, description = "Identificador da tarefa")
),
responses(
(status = 200, description = "Tarefa encontrada", body = TarefaResponse),
(status = 404, description = "Tarefa não encontrada", body = ErroApi)
)
)]
pub async fn obter_tarefa(
Path(id): Path<Uuid>,
) -> Result<Json<TarefaResponse>, (StatusCode, Json<ErroApi>)> {
let _ = id;
Err((
StatusCode::NOT_FOUND,
Json(ErroApi {
codigo: "nao_encontrada".into(),
mensagem: "Tarefa não encontrada".into(),
}),
))
}
Observe o alinhamento: o que a macro declara precisa corresponder ao que o handler realmente devolve. Documentar 201 e devolver 200 é exatamente o tipo de drift que OpenAPI deveria evitar. Em APIs maiores, extraia padrões de erro com thiserror e converta para ErroApi em um único lugar — o guia de tratamento de erros com thiserror e anyhow ajuda a manter essa camada limpa.
Monte o documento OpenApi
Agregue schemas, paths e metadados em um tipo dedicado:
use utoipa::{Modify, OpenApi};
use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme};
#[derive(OpenApi)]
#[openapi(
paths(
criar_tarefa,
obter_tarefa,
),
components(
schemas(CriarTarefaRequest, TarefaResponse, ErroApi)
),
tags(
(name = "tarefas", description = "CRUD de tarefas")
),
modifiers(&SecurityAddon),
info(
title = "API de Tarefas",
version = "1.0.0",
description = "Exemplo de contrato OpenAPI com Axum e utoipa"
)
)]
pub struct ApiDoc;
struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.get_or_insert_with(Default::default);
components.add_security_scheme(
"bearer_auth",
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
);
}
}
Esse ApiDoc::openapi() vira a fonte do JSON. Trate version como versão do contrato, não necessariamente a mesma do crate. Se você publica breaking changes em /v2, deixe isso explícito no info e nas tags.
Exponha JSON e Swagger UI no Axum
Com o documento pronto, sirva-o:
use axum::{routing::get, Router};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
pub fn app() -> Router {
Router::new()
.route("/tarefas", axum::routing::post(criar_tarefa))
.route("/tarefas/{id}", get(obter_tarefa))
.merge(
SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", ApiDoc::openapi()),
)
}
Em desenvolvimento local, abra /swagger-ui e valide os exemplos. Em produção, decida conscientemente:
- manter a UI apenas em staging;
- proteger com autenticação interna;
- publicar só o JSON para portais de desenvolvedores;
- ou expor a UI pública se a API for aberta.
Nunca trate Swagger UI como plano de autenticação. A UI é exploração; a autorização continua nos handlers e na camada Tower.
Integre autenticação, erros e versionamento
JWT Bearer
Declare o security scheme, marque paths protegidos e documente 401/403 de verdade. Clientes gerados a partir do OpenAPI conseguem preencher o header com menos tentativa e erro. Se o token carrega claims, não precisa expor a struct interna inteira: documente só o que o consumidor precisa saber.
Erros estáveis
Prefira códigos de erro legíveis (titulo_obrigatorio, rate_limited, conflito_versao) em vez de mensagens soltas traduzidas ad hoc. Isso facilita telemetria e retries. O mesmo cuidado aparece em serviços resilientes e em APIs gRPC com Tonic, onde o contrato também é o produto.
Versionamento
Estratégias comuns:
- path (
/v1/tarefas) — explícita e fácil de roteá; - header de versão — flexível, porém menos óbvia na UI;
- evolução compatível no mesmo path — ótima até o primeiro breaking change inevitável.
Qualquer que seja a escolha, o documento OpenAPI precisa refletir a realidade do roteador Axum. Gerar um JSON “bonito” que não corresponde às rotas piora a confiança.
Valide o contrato na CI
Documentação que não entra no pipeline envelhece. Algumas práticas simples:
- Compile os derives — se o schema quebra, o build quebra.
- Exporte o JSON em um job e faça diff contra o artefato anterior em PRs.
- Falhe em breaking changes quando o time adota política semver para a API.
- Smoke test de
/api-docs/openapi.jsonno ambiente de staging. - Revise exemplos quando um campo muda de significado sem mudar de nome.
Ferramentas do ecossistema Cargo que você já pode ter no toolbox — cargo-nextest, cargo-audit e o cluster de supply chain com cargo-deny — cuidam de testes e dependências. O contrato OpenAPI merece o mesmo rigor: é interface externa.
Se o time também trabalha com Go, compare mentalmente com a cultura de geração de clientes e specs do ecossistema reunido no Golang Brasil. A linguagem muda; a necessidade de contrato versionado não.
Checklist de portfólio e vagas
Um projeto Axum fica muito mais convincente para carreira Rust quando o README mostra:
- rotas reais com create/read/update;
- autenticação documentada no OpenAPI;
- erros com código estável e exemplos;
-
/api-docs/openapi.jsonacessível no ambiente de demo; - Swagger UI ou link para portal equivalente;
- testes de integração cobrindo status codes documentados;
- Docker Compose ou script de subida;
- nota sobre versão do contrato e política de breaking changes.
Recrutadores e avaliadores técnicos raramente executam todos os caminhos da API. Eles abrem a documentação. Se o contrato estiver claro, o projeto parece produto; se estiver ausente, parece exercício de framework.
Erros frequentes
Documentar só o happy path
Esquecer 400, 401, 404 e 409 empurra o consumidor para engenharia reversa de logs.
Reutilizar a entity do banco como schema público
Vaza colunas internas, timestamps confusos e campos que você não quer comprometer semver.
Deixar a UI aberta com dados reais em produção
Swagger UI sem proteção pode expor superfície desnecessária. Separe exploração de operação.
Mudar significado de campo sem bump de versão
status: String que deixa de aceitar valores antigos é breaking change disfarçado.
Gerar OpenAPI e nunca olhar o JSON
Macros ajudam, mas alguém precisa revisar o artefato como parte do PR.
Confundir OpenAPI com gRPC
Borda REST documenta com OpenAPI; serviços internos de alto throughput podem preferir Protobuf. Muitos sistemas usam os dois.
Perguntas frequentes
O que é utoipa no Rust?
É uma crate para gerar OpenAPI a partir de tipos e anotações Rust. Com ToSchema, utoipa::path e OpenApi, o contrato fica próximo do código compilado.
Como documentar uma API Axum com OpenAPI?
Modele schemas, anote handlers, agregue um OpenApi e sirva o JSON — com ou sem Swagger UI. Mantenha status codes e erros alinhados ao comportamento real.
utoipa substitui testes de contrato?
Não. A especificação descreve; os testes verificam. Use ambos: OpenAPI para comunicação e suíte automatizada para regressão.
Qual a diferença entre utoipa e Aide?
São abordagens diferentes no ecossistema Axum. utoipa é derive/macro-centric; Aide enfatiza outra integração com extratores e roteamento. Avalie pela ergonomia do time.
OpenAPI ajuda em vagas e portfólio Rust?
Ajuda bastante. Mostra preocupação com contrato, versionamento e experiência de quem consome a API — sinais de senioridade em backend.
Conclusão
OpenAPI não é burocracia de big tech: é a forma mais clara de dizer o que a sua API Axum aceita, devolve e rejeita. Com utoipa, essa descrição nasce dos mesmos tipos Serde que a aplicação já usa, e o Swagger UI transforma o contrato em ferramenta de exploração para o time e para quem avalia o seu portfólio.
Comece pequeno: duas rotas, um schema de erro estável, Bearer auth e o JSON versionado na CI. Depois evolua tags, exemplos e política de breaking changes. Para aprofundar o stack ao redor, volte ao guia de Axum, ao tutorial de API REST, ao comparativo SQLx vs Diesel vs SeaORM e às ferramentas essenciais do Cargo. Contrato bom não substitui código bom — mas código bom sem contrato envelhece mais rápido do que deveria.