Small, focused utilities for Rust HTTP APIs built with Axum and utoipa. The workspace favors types and generated code over runtime configuration or generic JSON maps.
Rust 96%
Nix 4%

README.md

tag_api #

Small, focused utilities for Rust HTTP APIs built with Axum. The workspace favors types and generated code over runtime configuration or generic JSON maps.

Documentation backends #

Every crate that documents itself for OpenAPI does so through opt-in features:

Feature Status Description
aide maintained Implements aide's OperationOutput/OperationInput, backed by schemars JSON schemas. Response schemas are collected automatically from the handler's return type.
schemars maintained Standalone JsonSchema implementations for crates without Axum handlers (tag_api_slug).
utoipa unmaintained The legacy utoipa 5 integration (IntoResponses, IntoParams, ToSchema). Not enabled by default and no longer developed; it exists so existing utoipa-based applications keep compiling.

The ProblemError derive always generates schemars-backed response specs through the runtime crate. When a caller declares utoipa, its legacy implementation remains guarded by that caller's utoipa feature.

use axum::Json;
use schemars::JsonSchema;
use serde::Serialize;
use tag_api_responses::Created;

/// A user.
#[derive(Serialize, JsonSchema)]
struct User {
    id: u64,
}

// With the `aide` feature, the 201 response and its body schema are inferred
// entirely from this return type:
async fn create_user() -> Created<Json<User>> {
    Created(Json(User { id: 42 }))
}

Crates #

Crate Purpose
tag_api_problem_details RFC 9457 error responses and OpenAPI response generation
tag_api_responses Common status response wrappers for Axum
tag_api_pagination Query pagination and RFC 8288 navigation links
tag_api_health_check Minimal GET /health endpoint
tag_api_slug Validated URL-friendly identifiers
tag_api_problem_details_derive Proc-macro implementation re-exported by tag_api_problem_details

The crates target Axum 0.8, aide 0.16 / schemars 1, and Rust edition 2024.

Problem Details (unmaintained utoipa example) #

The snippet below shows the unmaintained utoipa integration. With the aide feature the same enum documents itself through schemars; see the snapshot test in crates/problem_details/tests/aide_problem_details.rs for the generated document.

Define an endpoint's errors once. Fields stay inline in the enum, so a separate payload struct is not required:

use axum::{Json, extract::Path};
use tag_api_problem_details::{Problem, ProblemError};
use tag_api_responses::Okay;
use utoipa::ToSchema;

#[derive(serde::Serialize, ToSchema)]
struct User {
    id: String,
}

#[derive(ProblemError)]
enum UserError {
    /// The requested user does not exist.
    #[problem(status = 404)]
    ResourceNotFound { id: String },
}

/// Get a user.
///
/// Returns the user identified by `id`.
#[utoipa::path(
    get,
    path = "/users/{id}",
    params(("id", Path, description = "User identifier"))
)]
async fn get_user(
    Path(id): Path<String>,
) -> Result<Okay<Json<User>>, Problem<UserError>> {
    Err(Problem(UserError::ResourceNotFound { id }))
}

Enable Utoipa's axum_extras and auto_into_responses features for this form. The path parameter type and all responses come from the handler signature. The handler comment supplies the operation summary and description. For structured path or query extractors, derive IntoParams; field comments become parameter descriptions.

Problem<E> is a tuple wrapper, like Axum's Json<T> wrapper. The derive generates the response implementation and the utoipa response map. A problem response contains the generated type, title, status, and the variant fields:

{
  "type": "urn:tag-api-example:error:user-error:resource-not-found",
  "title": "The requested user does not exist.",
  "status": 404,
  "id": "42"
}

The default problem type is generated as:

urn:<cargo-package-name>:error:<error-enum>:<variant>

Names are normalized to kebab case. Set an enum-level namespace when the package name should not be used:

#[derive(ProblemError)]
#[problem(namespace = "tag-api-example")]
enum UserError {
    #[problem(status = 404)]
    ResourceNotFound { id: String },
}

The generated type value is also exposed as an associated constant such as UserError::PROBLEM_TYPE_RESOURCE_NOT_FOUND and is represented as a fixed value in the OpenAPI schema.

See the complete Axum, utoipa, and utoipa-axum application in examples/axum_utoipa.

Pagination #

tag_api_pagination provides bounded query parameters and a response wrapper for RFC 8288 Link headers:

use tag_api_pagination::{PaginatedBody, Pagination};

let body = PaginatedBody::new(items, total_count, &page);
let response = page.respond(body, Some("next-cursor"), None);

Use Pagination<Filters> as the handler extractor. It defaults limit to 20, clamps it to 1 through 100, and builds navigation links from the request URI.

Health Check #

tag_api_health_check::router() adds a minimal GET /health endpoint returning application/health+json:

let app = axum::Router::new().merge(tag_api_health_check::router());

Examples #

Run the maintained Axum + aide/schemars example from the workspace root:

cargo run -p tag_api_aide_example

Open http://127.0.0.1:3000/docs for Scalar, or request the endpoint and generated OpenAPI document directly:

curl http://127.0.0.1:3000/users/1
curl http://127.0.0.1:3000/users/missing
curl http://127.0.0.1:3000/openapi.json

The unmaintained utoipa example remains available for existing integrations:

cargo run -p tag_api_example

Development #

Run the standard checks from the workspace root:

cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
nix fmt -- --fail-on-change
nix flake check