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 std::cmp::Ordering; pub struct PrefixIndex { entries: Vec<String>,
Prefix search with binary partitioning in Rust
binary-search
sorting
case-insensitive
Intermediate
7 steps
rust
use std::cmp::Ordering; use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq)]
Parsing and ordering semantic versions in Rust
parsing
trait-implementation
ordering
Intermediate
8 steps
typescript
type ResizeCallback = (size: { width: number; height: number }) => void; export function observeResize(callback: ResizeCallback, delay = 150) { let timeoutId: ReturnType<typeof setTimeout> | undefined;
Debouncing window resize in TypeScript
debounce
closures
event-listeners
Intermediate
6 steps
rust
use std::collections::HashMap; use std::fs::File; use std::io::{self, BufRead, BufReader}; use std::path::Path;
Parsing INI files in Rust
parsing
file-io
hashmap
Intermediate
9 steps
rust
use axum::{ extract::{Path, Query}, http::Uri, response::{IntoResponse, Redirect},
Building HTTP redirect handlers in Axum
redirects
extractors
url-handling
Intermediate
7 steps
rust
use axum::{ body::Body, extract::MatchedPath, http::{Request, StatusCode},
An admin route guard in Axum middleware
middleware
authorization
request-extensions
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.