utoipa
Installation
SKILL.md
Utoipa v5.4 — OpenAPI 3.1 Generator for Rust
utoipa generates OpenAPI 3.1 specifications at compile time using Rust macros. It integrates perfectly with Axum, turning documented Rust structs and functions into fully compliant OpenAPI JSON.
Dependency Setup
[dependencies]
utoipa = { version = "5.4", features = ["axum_extras", "openapi_extensions"] }
moka = { version = "0.12", features = ["future"] }
serde_json = "1"
Core Patterns at a Glance
1. Documenting Types (ToSchema)
Use #[derive(ToSchema)] on structs and enums. Include examples for better developer experience.
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, ToSchema, Clone)]
pub struct TaskResult {
/// The unique task ID (ULID)
#[schema(example = "01HT7M5A20X8V2Z1N7G3P4Y9BQ")]
pub tid: String,
/// Execution status
#[schema(example = "done")]
pub status: String,
}
2. Documenting Routes (#[utoipa::path])
Annotate Axum handlers with #[utoipa::path]. Specify parameters, request bodies, and responses.
use axum::{extract::Path, Json, http::StatusCode};
#[utoipa::path(
get,
path = "/v1/tasks/{kind}/{tid}",
responses(
(status = 200, description = "Task found", body = TaskResult),
(status = 404, description = "Task not found")
),
params(
("kind" = String, Path, description = "Task kind slug"),
("tid" = String, Path, description = "Task ULID")
),
security(
("AltchaAuth" = [])
)
)]
pub async fn get_task(
Path((_kind, _tid)): Path<(String, String)>
) -> Result<Json<TaskResult>, StatusCode> {
Ok(Json(TaskResult { tid: "01HT...".into(), status: "done".into() }))
}
3. Assembling and Caching the Spec (Axum + Moka)
In high-performance orchestrators, the OpenAPI JSON should be generated once, cached in memory using moka, and served via Axum.
use axum::{routing::get, Router, Json, extract::State};
use moka::future::Cache;
use std::time::Duration;
use utoipa::OpenApi;
use serde_json::Value;
#[derive(OpenApi)]
#[openapi(
paths(get_task),
components(schemas(TaskResult)),
info(title = "Platform API", version = "1.0.0"),
tags((name = "tasks", description = "Task execution API"))
)]
struct ApiDoc;
#[derive(Clone)]
struct AppState {
// Cache the JSON Value, not the string, so Axum sets Content-Type correctly
openapi_cache: Cache<String, Value>,
}
pub fn router() -> Router {
let cache = Cache::builder()
.time_to_live(Duration::from_secs(3600)) // 1 hour TTL
.build();
Router::new()
.route("/v1/openapi.json", get(serve_openapi))
.with_state(AppState { openapi_cache: cache })
}
async fn serve_openapi(State(state): State<AppState>) -> Json<Value> {
let spec = state.openapi_cache.get_with("openapi", async {
// Generate the spec on cache miss
let doc = ApiDoc::openapi();
serde_json::to_value(&doc).unwrap()
}).await;
Json(spec)
}