---
title: "utoipa + Axum: OpenAPI e Swagger UI em Rust | Rust Brasil"
url: "https://rustlang.com.br/blog/utoipa-axum-openapi-swagger-ui-rust-2026/"
markdown_url: "https://rustlang.com.br/blog/utoipa-axum-openapi-swagger-ui-rust-2026.MD"
description: "Documente APIs Axum com utoipa: ToSchema, paths tipados, Swagger UI, JWT, versionamento do contrato OpenAPI e checklist de portfólio para vagas Rust em 2026."
date: "2026-09-01"
author: "Equipe Rust Brasil"
---

# utoipa + Axum: OpenAPI e Swagger UI em Rust | Rust Brasil

Documente APIs Axum com utoipa: ToSchema, paths tipados, Swagger UI, JWT, versionamento do contrato OpenAPI e checklist de portfólio para vagas Rust em 2026.


**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](/vagas/): “onde está o contrato da API?”. Times que já usam [Axum](/ecossistema/axum/), [Serde](/ecossistema/serde/), [SQLx](/ecossistema/sqlx/) e [Tower](/ecossistema/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](/tutoriais/api-rest-axum/) e pelo guia de [serviços resilientes com Tower e Axum](/blog/rust-servicos-resilientes-tower-axum-2026/).

## 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](/carreira/entrevista-rust-backend/) e no [nicho web/backend](/carreira/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:

```toml
[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.

```rust
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 `Option` de 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.

```rust
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](/blog/tratamento-erros-rust-thiserror-anyhow/) ajuda a manter essa camada limpa.

## Monte o documento OpenApi

Agregue schemas, paths e metadados em um tipo dedicado:

```rust
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:

```rust
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](/blog/rust-servicos-resilientes-tower-axum-2026/) e em APIs gRPC com [Tonic](/blog/rust-grpc-tonic-microservicos-2026/), onde o contrato também é o produto.

### Versionamento

Estratégias comuns:

1. path (`/v1/tarefas`) — explícita e fácil de roteá;
2. header de versão — flexível, porém menos óbvia na UI;
3. 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:

1. **Compile os derives** — se o schema quebra, o build quebra.
2. **Exporte o JSON em um job** e faça diff contra o artefato anterior em PRs.
3. **Falhe em breaking changes** quando o time adota política semver para a API.
4. **Smoke test** de `/api-docs/openapi.json` no ambiente de staging.
5. **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`](/blog/cargo-nextest-testes-rust-2026/), [`cargo-audit`](/blog/cargo-audit-vulnerabilidades-dependencias-rust-ci-2026/) e o cluster de [supply chain com cargo-deny](/blog/rust-seguranca-supply-chain-cargo-deny-sbom-2026/) — 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 <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Golang Brasil</a>. 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](/blog/carreira-rust-2026/) 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.json` acessí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](/ecossistema/axum/), ao [tutorial de API REST](/tutoriais/api-rest-axum/), ao comparativo [SQLx vs Diesel vs SeaORM](/blog/rust-banco-dados-sqlx-diesel-seaorm-2026/) e às [ferramentas essenciais do Cargo](/artigos/cargo-ferramentas-essenciais/). Contrato bom não substitui código bom — mas código bom sem contrato envelhece mais rápido do que deveria.
