rust
71 lines · 8 steps
Self-documenting routes in Axum
A builder wraps Axum's Router to record every route's metadata and expose it as an /openapi.json endpoint.
Explained by
highlit
1use axum::{
2 extract::State,
3 routing::{get, post, MethodRouter},
4 Json, Router,
5};
6use serde::Serialize;
7use std::sync::Arc;
8
9#[derive(Clone, Serialize)]
10struct RouteInfo {
11 method: &'static str,
12 path: String,
13 summary: &'static str,
14}
15
16#[derive(Clone, Default)]
17struct ApiCatalog {
18 routes: Vec<RouteInfo>,
19}
20
21struct DocumentedRouter<S> {
22 router: Router<S>,
23 catalog: ApiCatalog,
24}
25
26impl<S> DocumentedRouter<S>
27where
28 S: Clone + Send + Sync + 'static,
29{
30 fn new() -> Self {
31 Self {
32 router: Router::new(),
33 catalog: ApiCatalog::default(),
34 }
35 }
36
37 fn route(
38 mut self,
39 method: &'static str,
40 path: &str,
41 summary: &'static str,
42 handler: MethodRouter<S>,
43 ) -> Self {
44 self.catalog.routes.push(RouteInfo {
45 method,
46 path: path.to_string(),
47 summary,
48 });
49 self.router = self.router.route(path, handler);
50 self
51 }
52
53 fn finish(self) -> Router<S> {
54 let catalog = Arc::new(self.catalog);
55 self.router.route(
56 "/openapi.json",
57 get(move || {
58 let catalog = Arc::clone(&catalog);
59 async move { Json(catalog.routes.clone()) }
60 }),
61 )
62 }
63}
64
65pub fn build_api() -> Router {
66 DocumentedRouter::new()
67 .route("GET", "/users", "List all users", get(list_users))
68 .route("POST", "/users", "Create a user", post(create_user))
69 .route("GET", "/users/:id", "Fetch a single user", get(show_user))
70 .finish()
71}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Wrapping a framework type lets you capture side metadata every time you call its methods.
- 2A move closure that clones an Arc gives each request cheap shared access to immutable data.
- 3Threading self through builder methods keeps route registration and documentation in a single fluent chain.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
rust
use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Lines, Write};
K-way merge of sorted logs in Rust
binary-heap
k-way-merge
streaming-io
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
rust
use std::f64::consts::PI; #[derive(Debug, Clone, Copy)] pub struct LatLng {
Building geographic bounding boxes in Rust
geospatial
value-types
option
Intermediate
7 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/self-documenting-routes-in-axum-explained-rust-fdc2/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.