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
aidefeature the same enum documents itself through schemars; see the snapshot test incrates/problem_details/tests/aide_problem_details.rsfor 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