Skip to main content

Crate doxa_docs

Crate doxa_docs 

Source
Expand description

§doxa

Ergonomic OpenAPI documentation for axum services. Built on top of utoipa and utoipa_axum, this crate provides:

  • An ApiDocBuilder for assembling an OpenAPI document from a utoipa::openapi::OpenApi value, finalizing it into an in-memory ApiDoc whose serialized JSON is shared via a reference-counted bytes::Bytes buffer.
  • A mount_docs helper that mounts GET /openapi.json plus an interactive documentation UI on an existing axum::Router, all from memory — the spec is never written to disk.
  • An RFC 7807 ProblemDetails response body usable as the default error schema across a project.

All UI integrations are feature-gated independently. The default feature set enables docs-scalar which mounts the Scalar API reference UI from a CDN-loaded HTML template, rendered out of the box with the three-pane modern layout, dark mode on, the schemas index hidden, the codegen sidebar suppressed, and Scalar’s paid product upsells (Agent / MCP) disabled. Every one of those choices is overridable via ScalarConfig passed through MountOpts::scalar. Scalar is preferred because it is actively maintained, parses OpenAPI 3.2 natively, renders the x-badges vendor extension, and surfaces required OAuth2 scopes inline under each operation — covering per-operation permission requirements produced by extractor-side DocOperationSecurity impls.

§Tour

The full surface of the crate, end to end. Every macro, derive, extractor, and builder method that ships in the default feature set appears in the snippet below and the whole thing compiles under cargo test --doc.

use axum::Json;
use doxa::{
    routes, ApiDocBuilder, ApiErrorBody, ApiResult, DocumentedHeader, Header,
    MountDocsExt, MountOpts, OpenApiRouter, ScalarConfig, ScalarLayout, ScalarTheme,
    SseEvent, SseEventMeta, SseSpecVersion, SseStream, ToSchema,
};
use doxa::{get, post, ApiError};
use futures_core::Stream;
use serde::{Deserialize, Serialize};
use std::convert::Infallible;

// ----- Typed error envelope ----------------------------------------------
//
// `#[derive(ApiError)]` wires both `IntoResponse` and `IntoResponses`
// from per-variant `#[api(status, code)]` attributes. Multiple
// variants may share a status — they are grouped into one OpenAPI
// response with separate examples.
#[derive(Debug, thiserror::Error, Serialize, ToSchema, ApiError)]
enum WidgetError {
    #[error("validation failed: {0}")]
    #[api(status = 400, code = "validation_error")]
    Validation(String),

    #[error("conflict: {0}")]
    #[api(status = 400, code = "conflict")]
    Conflict(String),

    #[error("not found")]
    #[api(status = 404, code = "not_found")]
    NotFound,

    #[error("internal error")]
    #[api(status = 500, code = "internal")]
    Internal,
}

// ----- Typed request / response bodies -----------------------------------
#[derive(Debug, Serialize, ToSchema)]
struct Widget { id: u32, name: String }

#[derive(Debug, Deserialize, ToSchema)]
struct CreateWidget { name: String }

// ----- Typed header extractor --------------------------------------------
//
// Implementing `DocumentedHeader` on a marker type lets the same
// marker drive both extraction (via `Header<XApiKey>`) and OpenAPI
// documentation. The macro recognizes `Header<H>` in the handler
// signature and emits the corresponding params block automatically.
struct XApiKey;
impl DocumentedHeader for XApiKey {
    fn name() -> &'static str { "X-Api-Key" }
    fn description() -> &'static str { "Tenant API key" }
}

// ----- SSE event stream --------------------------------------------------
//
// `#[derive(SseEvent)]` provides the per-variant event name; pair
// it with `serde::Serialize` and `ToSchema` so the wire format and
// OpenAPI schema stay aligned. `SseStream<E, S>` is the response
// wrapper — handlers never construct axum's `Sse` directly.
#[derive(Serialize, ToSchema, SseEvent)]
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
enum BuildEvent {
    Started { id: u64 },
    Progress { done: u64, total: u64 },
    #[sse(name = "finished")]
    Completed,
}

// ----- Handlers ----------------------------------------------------------
/// Create a widget. The path uses the `#[post]` shortcut, takes a
/// typed JSON body and a typed header, and returns an
/// `ApiResult<Json<T>, E>` so successes and the full error
/// vocabulary both flow into the OpenAPI document.
#[post("/widgets", tag = "Widgets")]
async fn create_widget(
    Header(_key, ..): Header<XApiKey>,
    Json(req): Json<CreateWidget>,
) -> ApiResult<(axum::http::StatusCode, Json<Widget>), WidgetError> {
    if req.name.is_empty() {
        return Err(WidgetError::Validation("name is required".into()));
    }
    Ok((
        axum::http::StatusCode::CREATED,
        Json(Widget { id: 42, name: req.name }),
    ))
}

/// Stream build progress as Server-Sent Events. The macro
/// recognizes `SseStream<E, _>` and emits a `text/event-stream`
/// response with one `oneOf` branch per `SseEvent` variant.
#[get("/builds/{id}/events", tag = "Builds")]
async fn stream_build(
) -> SseStream<BuildEvent, impl Stream<Item = Result<BuildEvent, Infallible>>> {
    let events = futures::stream::iter(vec![
        Ok(BuildEvent::Started { id: 1 }),
        Ok(BuildEvent::Progress { done: 1, total: 10 }),
        Ok(BuildEvent::Completed),
    ]);
    SseStream::new(events)
}

// ----- Compose, finalize, mount -----------------------------------------
let (router, openapi) = OpenApiRouter::<()>::new()
    .routes(routes!(create_widget))
    .routes(routes!(stream_build))
    .split_for_parts();

let api_doc = ApiDocBuilder::new()
    .title("Widgets API")
    .version("1.0.0")
    .description("Tour service")
    .bearer_security("bearerAuth")
    .tag_group("Core", ["Widgets"])
    .tag_group("Streaming", ["Builds"])
    // Use OpenAPI 3.2 `itemSchema` for SSE responses (the default).
    .sse_openapi_version(SseSpecVersion::V3_2)
    .merge(openapi)
    .build();

// Customize the Scalar UI: classic single-column layout with a
// light theme, dark mode off. `MountOpts::default()` keeps the
// historical three-pane modern dark-mode appearance.
let app = router.mount_docs(
    api_doc,
    MountOpts::default()
        .scalar(
            ScalarConfig::default()
                .layout(ScalarLayout::Classic)
                .theme(ScalarTheme::Solarized)
                .dark_mode(false),
        ),
);

The crate’s public surface contains no project-specific types — everything is generic over utoipa’s native types so it can be lifted into any axum project.

Macros§

routes
Wraps utoipa_axum::routes! and augments the schemas vector with everything each handler’s argument types reference.

Structs§

ApiDoc
Immutable, in-memory OpenAPI document with its serialized JSON form pre-rendered into a Bytes buffer.
ApiDocBuilder
Builder for an ApiDoc.
ApiErrorBody
Envelope for HTTP error responses produced by #[derive(ApiError)].
BadgeContribution
One badge entry the layer attaches to every operation it covers. Surfaces in doc UIs that render the x-badges vendor extension (Scalar): a colored chip with the supplied name.
DocHeaderEntry
Generic IntoParams implementor that produces one header parameter from a DocumentedHeader marker, calling H::name, H::description, and H::example at runtime.
Header
Extracts a header value by name, where the name is supplied type-level via DocumentedHeader::name. Modeled after axum_extra::TypedHeader but using our own DocumentedHeader trait so the macro pass can resolve the wire name at runtime without depending on the foreign headers crate.
HeaderParam
One header parameter descriptor. Construct via HeaderParam::typed / HeaderParam::typed_optional for type-safe declarations driven by DocumentedHeader, or via HeaderParam::required / HeaderParam::optional for ad-hoc string names.
LayerContribution
What a layer contributes to the OpenAPI contract for the operations it covers. Build with LayerContribution::new and the chainable with_* setters; empty fields are zero-cost.
MountOpts
Configuration for mount_docs.
OpenApi
Root object of the OpenAPI document.
OpenApiRouter
A wrapper struct for axum::Router and utoipa::openapi::OpenApi for composing handlers and services with collecting OpenAPI information from the handlers.
ProblemDetails
RFC 7807 problem details body.
ResponseContribution
One extra response status the layer can return (e.g. 401 from auth middleware, 429 from a rate limiter). Skipped on operations that already declare a response with the same status — handler-level declarations always win.
ScalarConfig
Scalar UI rendering options.
SecurityContribution
One security requirement entry the layer enforces. References a scheme that has been registered with crate::ApiDocBuilder::bearer_security or crate::ApiDocBuilder::security_scheme — a dangling reference produces an invalid spec, so make sure the scheme name matches.
SseStream
A typed SSE response stream.

Enums§

BuildError
Errors that can occur while ApiDocBuilder::building an ApiDoc.
DeveloperTools
Visibility of Scalar’s developer-tools drawer.
DocumentDownload
Format(s) offered by the header “Download OpenAPI” button.
ScalarLayout
Page layout variants.
ScalarTheme
Visual theme presets recognized by Scalar.
SseSpecVersion
OpenAPI spec version used to render Server-Sent Event (SSE) responses in the generated document.

Traits§

ApidocHandlerOps
Per-handler hook that mutates the Operation generated by #[utoipa::path] for this handler. Implemented by the method macro on the same __path_<fn> struct that carries the utoipa path metadata; the impl iterates every handler argument through the crate::DocOperationSecurity probe.
ApidocHandlerSchemas
Reports the full set of schemas a handler’s arguments reference. The method macro emits an impl for the dispatch struct; the extended routes! macro calls it to extend the OpenAPI router’s schema collection before the router is merged.
DocHeaderParams
Contributes header parameters to an operation.
DocOperationSecurity
Extractor-side contribution of per-operation security/permission metadata. Implemented by per-route guards (e.g. a permission extractor that names the action it requires) so the resulting OpenAPI operation documents the requirement.
DocPathParams
Contributes path parameters to an operation.
DocPathScalar
Scalar/tuple path impl trait. Kept separate from DocPathParams to avoid overlap with the struct-form impl.
DocQueryParams
Contributes query parameters to an operation.
DocRequestBody
Contributes the request body schema to an operation.
DocResponseBody
Describe how a handler’s return type contributes to its OpenAPI operation’s 200 response.
DocumentedHeader
Type-level descriptor for one HTTP header.
DocumentedLayer
A tower Layer that declares its own OpenAPI contribution.
InnerToSchema
Contributes schemas referenced by an extractor’s inner payload into the OpenAPI document’s component registry.
IntoParams
Trait used to convert implementing type to OpenAPI parameters.
IntoResponses
This trait is implemented to document a type (like an enum) which can represent multiple responses, to be used in operation.
MountDocsExt
Extension trait providing mount_docs as a fluent method on axum::Router. Equivalent to the free function mount_docs.
OpenApiRouterExt
Adds OpenApiRouterExt::layer_documented and OpenApiRouterExt::tag_all to OpenApiRouter. Implemented for every state type the underlying router supports.
PathScalar
Sealed trait for primitives usable as scalar Path parameters. Implementations supply the OpenAPI schema for the parameter — done manually (rather than via utoipa::PartialSchema) because common scalar path types like Uuid are recognized by utoipa only via token inspection in derives, not through a PartialSchema impl.
SseEventMeta
Per-variant metadata for a Server-Sent Event enum.
ToSchema
Trait for implementing OpenAPI Schema object.

Functions§

apply_badge_to_operation
Append an x-badges entry shaped for Scalar’s native badge renderer ({name, color}) to a single operation. Idempotent on name — calling twice with the same name leaves a single badge entry. color accepts any CSS color value (keyword, hex, rgb, hsl, or a Scalar CSS custom property like var(--scalar-color-accent)).
apply_contribution
Apply a contribution to every operation in openapi. Public so callers who hold an utoipa::openapi::OpenApi directly can also annotate it without going through crate::OpenApiRouterExt.
mount_docs
Mount the OpenAPI JSON endpoint (and a documentation UI, if a UI feature is enabled) on the supplied router.
operation_for_method_mut
Find the operation slot on a PathItem for a given HttpMethod. Used by the method-macro-generated ApidocHandlerOps impl to pick out the operation matching the handler’s declared methods.
record_required_permission
Record a per-operation permission requirement on op, emitting:

Type Aliases§

ApiResult
Convenience alias for handler return types whose error half implements IntoResponses. Equivalent to Result<T, E> but signals intent.

Attribute Macros§

capability
Declare a Capable marker type backed by a Capability constant.
delete
#[delete("/path", ...)] shortcut for [utoipa::path]. See get for the inference rules.
get
Shortcut for #[utoipa::path(get, path = "...")].
operation
Generic operation attribute for cases where the HTTP method must be specified explicitly (multi-method routes, non-standard verbs).
patch
#[patch("/path", ...)] shortcut for [utoipa::path]. See get for the inference rules.
post
#[post("/path", ...)] shortcut for [utoipa::path]. See get for the inference rules.
put
#[put("/path", ...)] shortcut for [utoipa::path]. See get for the inference rules.

Derive Macros§

ApiError
Derive [axum::response::IntoResponse] and [utoipa::IntoResponses] for an error enum from a single per-variant declaration.
IntoParams
Generate path parameters from struct’s fields.
IntoResponses
Generate responses with status codes what can be attached to the utoipa::path.
SseEvent
Derive SseEventMeta for an enum whose variants represent the events of a Server-Sent Event stream.
ToSchema
Generate reusable OpenAPI schema to be used together with OpenApi.