Skip to main content

axum_api_kit/
lib.rs

1//! Shared response types for Axum JSON APIs.
2//!
3//! Provides building blocks that every Axum CRUD service needs but always
4//! re-defines from scratch:
5//!
6//! - [`ApiError`] - a machine-readable JSON error body with `code`, `message`, and optional
7//!   `details`, plus factory helpers that return `(StatusCode, Json<ApiError>)` tuples ready
8//!   for use with Axum's [`IntoResponse`](axum::response::IntoResponse). The
9//!   `too_many_requests_with_retry_after` and `service_unavailable_with_retry_after`
10//!   factories additionally emit a delay-seconds `Retry-After` header. Supports `From`
11//!   conversions for common error types. With the optional `validator` feature enabled,
12//!   also supports converting `validator::ValidationErrors` into structured field errors.
13//!   With the optional `sqlx` feature enabled, also supports converting `sqlx::Error` into
14//!   semantically correct HTTP status codes (404, 409, 422, 503, 500).
15//! - [`ListResponse<T>`] - a generic offset/limit paginated collection response with `data`,
16//!   `total`, `limit`, and `offset` fields.
17//! - [`CursorResponse<T>`] - a generic cursor-based paginated collection response for large
18//!   datasets or feeds, with `data`, `next_cursor`, and `has_more` fields.
19//! - [`HealthResponse`] - a health-check response with `status` field supporting `ok`,
20//!   `degraded`, and `unhealthy` states.
21//! - [`Created<T>`], [`Accepted<T>`], and [`NoContent`] - success-side responses for the
22//!   rest of the CRUD lifecycle: `201 Created` (with an optional `Location` header),
23//!   `202 Accepted`, and `204 No Content`.
24//!
25//! With optional feature flags enabled, the kit also provides request extractors that
26//! reject with an [`ApiError`] body on failure:
27//!
28//! - `ValidatedJson<T>` (feature `validator`) - deserializes a JSON body and runs
29//!   `validator` validation before the handler runs.
30//! - `Pagination` and `CursorPagination` (feature `extract`) - parse `limit`/`offset` and
31//!   `cursor`/`limit` query parameters into typed values, with `list_response` /
32//!   `cursor_response` helpers that build the matching response type.
33//! - `ApiJson<T>` (feature `extract`) - a drop-in replacement for `axum::Json` whose
34//!   extraction failures reject with an `ApiError` body instead of Axum's plain-text default.
35//!
36//! It also ships observability middleware:
37//!
38//! - `propagate_request_id` and `trace_requests` (feature `trace`) - assign an
39//!   `x-request-id` correlation id (extractable via `RequestId`) and emit a structured
40//!   `tracing` event with method, path, status, and latency for each request.
41//!
42//! And service-wiring helpers:
43//!
44//! - `health_routes` and `liveness` (feature `router`) - a `Router` exposing `/healthz` and
45//!   `/readyz` probes backed by `HealthResponse`.
46//! - `cors_allowing` and `cors_permissive` (feature `cors`) - build a `tower_http`
47//!   `CorsLayer` with sensible defaults.
48//!
49//! And an RFC 9457 error format:
50//!
51//! - `Problem` (feature `problem`) - an RFC 9457 problem+json response type with builder
52//!   methods, lossless bridges from [`ApiError`], and Retry-After support. Emits
53//!   `Content-Type: application/problem+json`, with opt-in Accept-header content
54//!   negotiation (the `ProblemFormat` extractor plus `Problem::into_response_for` /
55//!   `Problem::into_response_with`) that serves the same body as plain `application/json`
56//!   only when the client strictly prefers it.
57//! - `ProblemJson<T>` (features `problem` + `extract`) and `ProblemValidatedJson<T>`
58//!   (features `problem` + `validator`) - problem-flavored siblings of `ApiJson` /
59//!   `ValidatedJson` whose extraction failures reject with an RFC 9457 body via
60//!   `ProblemRejection` (same status codes, same `code` and field-level details,
61//!   `Content-Type` negotiated via `ProblemFormat`). The existing extractors' rejection
62//!   bodies never change; the format is chosen by naming the extractor in the handler.
63//!
64//! With the `openapi` feature, all four response types derive `utoipa::ToSchema` so they
65//! can be referenced from a `utoipa` `OpenApi` document and appear in generated specs.
66//!
67//! # Quick Start
68//!
69//! ```rust,no_run
70//! use axum::{Json, http::StatusCode, response::IntoResponse};
71//! use axum_api_kit::{ApiError, ListResponse, CursorResponse, HealthResponse};
72//! use serde::Serialize;
73//!
74//! #[derive(Serialize)]
75//! struct Item { id: String }
76//!
77//! async fn list_items() -> impl IntoResponse {
78//!     let items = vec![Item { id: "1".into() }];
79//!     Json(ListResponse { data: items, total: 1, limit: 50, offset: 0 })
80//! }
81//!
82//! async fn feed_items(cursor: Option<String>) -> impl IntoResponse {
83//!     let items = vec![Item { id: "1".into() }];
84//!     CursorResponse { data: items, next_cursor: Some("abc".into()), has_more: true }
85//! }
86//!
87//! async fn get_item() -> impl IntoResponse {
88//!     ApiError::not_found("item not found")
89//! }
90//!
91//! async fn health() -> impl IntoResponse {
92//!     HealthResponse::ok()
93//! }
94//! ```
95
96#[cfg(feature = "extract")]
97mod apijson;
98#[cfg(feature = "cors")]
99mod cors;
100mod cursor;
101mod error;
102mod health;
103mod list;
104#[cfg(feature = "extract")]
105mod pagination;
106#[cfg(feature = "problem")]
107mod problem;
108#[cfg(all(feature = "problem", any(feature = "extract", feature = "validator")))]
109mod problemjson;
110#[cfg(feature = "router")]
111mod router;
112mod success;
113#[cfg(feature = "trace")]
114mod trace;
115#[cfg(feature = "validator")]
116mod validated;
117
118#[cfg(feature = "extract")]
119pub use apijson::ApiJson;
120#[cfg(feature = "cors")]
121pub use cors::{cors_allowing, cors_permissive};
122pub use cursor::CursorResponse;
123pub use error::ApiError;
124pub use health::HealthResponse;
125pub use list::ListResponse;
126#[cfg(feature = "extract")]
127pub use pagination::{CursorPagination, Pagination};
128#[cfg(feature = "problem")]
129pub use problem::{Problem, ProblemFormat, APPLICATION_PROBLEM_JSON};
130#[cfg(all(feature = "problem", feature = "extract"))]
131pub use problemjson::ProblemJson;
132#[cfg(all(feature = "problem", any(feature = "extract", feature = "validator")))]
133pub use problemjson::ProblemRejection;
134#[cfg(all(feature = "problem", feature = "validator"))]
135pub use problemjson::ProblemValidatedJson;
136#[cfg(feature = "router")]
137pub use router::{health_routes, liveness};
138pub use success::{Accepted, Created, NoContent};
139#[cfg(feature = "trace")]
140pub use trace::{propagate_request_id, trace_requests, RequestId, REQUEST_ID_HEADER};
141#[cfg(feature = "validator")]
142pub use validated::ValidatedJson;