axum-impl-openapi
>
Works with
---
name: axum-impl-openapi
description: >
license: MIT
---
# axum-impl-openapi
## Overview
Axum produces no API documentation of its own. The idiomatic solution is the
`utoipa` family of crates, which generates an OpenAPI 3 specification from Rust
annotations at compile time. Because the spec is derived from the same types
and handlers the server runs, it cannot silently drift from the code.
Three crates, each versioned independently:
- `utoipa` (5.5.0): the derive and attribute macros that annotate types and
handlers.
- `utoipa-swagger-ui` (9.0.2): bundles and serves the interactive Swagger UI.
- `utoipa-axum` (0.2.0): supplies `OpenApiRouter`, which registers a handler
ONCE for both routing and the spec, removing the last drift risk.
These three version numbers are NOT synchronised. ALWAYS pin all three
explicitly. The `utoipa` 4.x to 5.x bump changed macro behaviour; all code
here is valid for the 5.x line. Because `utoipa-axum` is still pre-1.0, treat
its `OpenApiRouter` surface as version-sensitive: a future `0.x` release may
rename `routes!`, `split_for_parts`, or the constructors.
## Quick Reference
Cargo dependencies (pin all three; versions verified 2026-05-20):
```toml
# version-sensitive: these three crates version independently
utoipa = { version = "5", features = ["axum_extras"] }
utoipa-axum = "0.2"
utoipa-swagger-ui = { version = "9", features = ["axum"] }
```
The `utoipa` macro surface:
| Macro | Kind | Applies to | Generates |
|-------|------|------------|-----------|
| `ToSchema` | derive | a type | a reusable OpenAPI schema component |
| `#[utoipa::path(...)]` | attribute | a handler `fn` | one OpenAPI operation |
| `OpenApi` | derive | a marker struct | the root OpenAPI document |
| `IntoParams` | derive | a query/path struct | grouped parameter definitions |
| `IntoResponses` | derive | a response enum/struct | grouped response definitions |
| `ToResponse` | derive | a type | a reusable response component |
Core rules:
- ALWAYS pin `utoipa`, `utoipa-axum`, and `utoipa-swagger-ui` to explicit,
independent versions. NEVER assume their version numbers match.
- ALWAYS write `#[utoipa::path]` `path` arguments with the curly-brace
`{param}` syntax. The macro requires it on BOTH Axum 0.7 and 0.8.
- ALWAYS put the HTTP operation (`get`, `post`, ...) as the FIRST argument of
`#[utoipa::path]`.
- ALWAYS prefer `utoipa-axum`'s `OpenApiRouter` for a new service: it registers
each handler once and cannot drift.
- ALWAYS enable the `axum` feature on `utoipa-swagger-ui`; without it
`SwaggerUi` cannot become an Axum router.
- NEVER hand-write or hand-edit the OpenAPI JSON. The whole point is that the
spec is generated.
## Decision Trees
### Which integration approach to use
```
Building the OpenAPI integration for an Axum service:
new service, want routing and spec from one source of truth
-> utoipa-axum OpenApiRouter + routes!() (register a handler once)
existing Router, adding docs incrementally, cannot restructure routing
-> plain utoipa: #[derive(OpenApi)] with #[openapi(paths(...))]
(register each handler twice, once routed, once in paths(...))
need only the spec file, no interactive UI
-> utoipa alone; serialise with ApiDoc::openapi().to_pretty_json()
```
Plain `utoipa` requires registering every handler twice: once on the Axum
`Router` and once inside `#[openapi(paths(...))]`. Forgetting one half means a
route exists but is undocumented, or a documented operation has no route.
`OpenApiRouter` derives both from a single `.routes(routes!(...))` call, so the
two cannot disagree. ALWAYS pick `OpenApiRouter` for a new service.
### The {param} versus :id path-syntax decision
```
Writing the path string for a documented route:
the #[utoipa::path] annotation -> ALWAYS {param} (e.g. "/pets/{id}")
the Axum route string on 0.8 -> {param} -> identical to utoipa
the Axum route string on 0.7 -> :param -> deliberately differs
```
`#[utoipa::path]` requires the OpenAPI brace form `{id}` on every Axum version.
Axum 0.8's native route syntax is also `{id}`, so on 0.8 the route string and
the `utoipa::path` annotation are character-for-character identical. On Axum
0.7 the route uses `:id` while the annotation still uses `{id}`; the two
strings differ on purpose. `OpenApiRouter` with `routes!()` removes the issue
entirely: it derives the route from the annotation, so there is one string.
## Patterns
### Pattern: a schema with #[derive(ToSchema)]
`#[derive(ToSchema)]` turns a type into a reusable OpenAPI schema component.
Doc comments become the OpenAPI `description`. Field-level `#[schema(...)]`
attributes refine the generated schema.
```rust
// utoipa 5.x
use utoipa::ToSchema;
#[derive(ToSchema, serde::Serialize)]
struct Pet {
/// Unique database id of the pet.
id: u64,
name: String,
#[schema(maximum = 30, minimum = 0)]
age: Option<i32>,
}
```
When both a serde attribute and a `#[schema]` equivalent are present on a
field, the serde attribute takes precedence. Every generic type parameter must
itself implement `ToSchema`.
### Pattern: documenting a handler with #[utoipa::path]
`#[utoipa::path(...)]` documents one endpoint. The HTTP operation MUST be the
first argument. The `path` uses the `{param}` brace syntax.
```rust
// utoipa 5.x - the {param} path syntax is required on Axum 0.7 AND 0.8
#[utoipa::path(
get,
path = "/pets/{id}",
responses(
(status = 200, description = "Pet found successfully", body = Pet),
(status = NOT_FOUND, description = "Pet was not found")
),
params(
("id" = u64, Path, description = "Pet database id")
)
)]
async fn get_pet_by_id(/* extractors */) { /* ... */ }
```
`responses(...)` entries are `(status = ..., description = "...", body = ...)`
tuples; `body` is optional. `params(...)` accepts the tuple form shown or an
`IntoParams` struct.
### Pattern: assembling the root document with #[derive(OpenApi)]
`#[derive(OpenApi)]` builds the root OpenAPI document on a marker struct.
`paths(...)` registers annotated handlers; `components(schemas(...))` registers
`ToSchema` types. The derive implements the `OpenApi` trait, whose `openapi()`
function returns the `utoipa::openapi::OpenApi` value.
```rust
// utoipa 5.x
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(get_pet_by_id),
components(schemas(Pet))
)]
struct ApiDoc;
// ApiDoc::openapi() yields the spec; .to_pretty_json() serialises it
println!("{}", ApiDoc::openapi().to_pretty_json().unwrap());
```
This is the plain-`utoipa` approach: `get_pet_by_id` must ALSO be registered on
the Axum `Router` separately. The next pattern removes that duplication.
### Pattern: OpenApiRouter registers a handler once
`utoipa-axum`'s `OpenApiRouter` collects handlers through the `routes!()`
macro, deriving the HTTP method and path from each `#[utoipa::path]`
annotation. `.split_for_parts()` consumes the router and returns the runnable
`axum::Router` plus the finished `OpenApi` value.
```rust
// utoipa-axum 0.2.x - version-sensitive API; pin utoipa-axum = "0.2"
use utoipa_axum::router::OpenApiRouter;
use utoipa_axum::routes;
let (router, api): (axum::Router, utoipa::openapi::OpenApi) =
OpenApiRouter::new()
.routes(routes!(get_pet_by_id))
.split_for_parts();
```
`OpenApiRouter::with_openapi(openapi)` seeds the router with an existing
`OpenApi` value, so top-level `info`, `tags`, and `servers` from a
`#[derive(OpenApi)]` marker struct carry through. `.nest(path, router)` and
`.merge(router)` compose `OpenApiRouter`s exactly like the Axum `Router`
methods while keeping the spec consistent.
### Pattern: serving the Swagger UI
`utoipa-swagger-ui`'s `SwaggerUi` serves the interactive UI and the raw spec
JSON. With the `axum` feature it converts into an Axum router and attaches via
`Router::merge`.
```rust
// utoipa-swagger-ui 9.x - the `axum` feature is mandatory for merge
use utoipa_swagger_ui::SwaggerUi;
let app = Router::new()
.route("/pets/{id}", get(get_pet_by_id)) // axum 0.8 route string
.merge(
SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", ApiDoc::openapi()),
);
```
`SwaggerUi::new("/swagger-ui")` mounts the UI; `.url(json_path, spec)`
registers the spec at a JSON endpoint. On Axum 0.7 the route string is
`"/pets/:id"` while the `utoipa::path` annotation stays `"/pets/{id}"`.
### Pattern: the full three-crate integration
The idiomatic setup builds an `OpenApiRouter`, splits it, and feeds the
produced spec into `SwaggerUi`. Routing, schemas, and the UI all derive from
one registration.
```rust
// utoipa 5.x + utoipa-axum 0.2.x + utoipa-swagger-ui 9.x
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;
use utoipa_axum::routes;
use utoipa_swagger_ui::SwaggerUi;
#[derive(OpenApi)]
#[openapi(components(schemas(Pet)))]
struct ApiDoc;
let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
.routes(routes!(get_pet_by_id))
.split_for_parts();
let app = router.merge(
SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", api),
);
```
`get_pet_by_id` is registered exactly once, in `routes!()`. Its route and its
spec operation are both derived from the single `#[utoipa::path]` annotation,
so they cannot drift.
## Reference Links
- `references/methods.md`: the full `utoipa` macro surface, every verified
`#[utoipa::path]` and `#[schema]` argument, the `OpenApi` trait, the
`OpenApiRouter` method list with signatures, `SwaggerUi`, and the feature
flags of all three crates.
- `references/examples.md`: a complete `Cargo.toml`, schema and handler
annotations, the plain-`utoipa` assembly, the `OpenApiRouter` full
integration, `IntoParams` for query structs, and serving the raw spec.
- `references/anti-patterns.md`: real mistakes with root-cause analysis,
including hand-editing the spec, the route missing from `paths(...)`, the
`:id` versus `{id}` mismatch, mismatched crate versions, the missing `axum`
feature, and the operation argument placed after `path`.
Related skills: `axum-core-router` (`{id}` versus `:id` route syntax across
versions, `.nest` and `.merge`), `axum-syntax-handlers` (the handler signatures
that `#[utoipa::path]` annotates), `axum-core-version-migration` (the 0.7-to-0.8
route-syntax change).More API Design skills
lark-event
larksuite/cli
Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume <EventKey>` (covers IM messages/reactions/chat changes, Approval status changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses.
lark-contact
larksuite/cli
飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名,以及按关键词搜索当前用户可见的机器人 / 智能体(agent)。当用户提到一个名字要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。
lark-openapi-explorer
larksuite/cli
飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。

