rust
53 lines · 8 steps
Multi-tenant DB routing in Axum extractors
A custom Axum extractor resolves the tenant from a request's subdomain and hands the handler the right database pool.
Explained by
highlit
1use axum::{
2 extract::{FromRequestParts, Host},
3 http::{request::Parts, StatusCode},
4};
5use sqlx::PgPool;
6use std::sync::Arc;
7
8#[derive(Clone)]
9pub struct TenantRegistry {
10 pools: Arc<dashmap::DashMap<String, PgPool>>,
11}
12
13impl TenantRegistry {
14 pub async fn pool_for(&self, slug: &str) -> Option<PgPool> {
15 self.pools.get(slug).map(|p| p.clone())
16 }
17}
18
19pub struct TenantDb {
20 pub slug: String,
21 pub pool: PgPool,
22}
23
24impl<S> FromRequestParts<S> for TenantDb
25where
26 TenantRegistry: FromRef<S>,
27 S: Send + Sync,
28{
29 type Rejection = (StatusCode, &'static str);
30
31 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
32 let Host(host) = Host::from_request_parts(parts, state)
33 .await
34 .map_err(|_| (StatusCode::BAD_REQUEST, "missing host header"))?;
35
36 let slug = host
37 .split('.')
38 .next()
39 .filter(|s| !s.is_empty() && *s != "www")
40 .ok_or((StatusCode::BAD_REQUEST, "could not resolve tenant subdomain"))?
41 .to_owned();
42
43 let registry = TenantRegistry::from_ref(state);
44 let pool = registry
45 .pool_for(&slug)
46 .await
47 .ok_or((StatusCode::NOT_FOUND, "unknown tenant"))?;
48
49 Ok(TenantDb { slug, pool })
50 }
51}
52
53use axum::extract::FromRef;
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Custom extractors let you turn cross-cutting request logic into a typed handler argument.
- 2FromRef decouples an extractor from the concrete app state, so it composes with any state that can produce a TenantRegistry.
- 3Mapping each failure mode to a distinct status code keeps rejection handling explicit and honest.
Related explainers
rust
#[derive(Debug, Clone, PartialEq)] pub enum Token { Number(f64), Plus,
How a tokenizer turns text into tokens
lexing
enums
iterators
Intermediate
8 steps
javascript
import { useState } from 'react'; export function ReorderableList({ initialItems }) { const [items, setItems] = useState(initialItems);
Drag-to-reorder lists in React
drag-and-drop
state-management
immutable-updates
Intermediate
8 steps
rust
use std::{collections::HashSet, sync::Arc}; use axum::{ body::Body,
Feature-flag middleware in Axum
middleware
async
shared-state
Advanced
7 steps
java
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class TenantResolutionFilter extends OncePerRequestFilter {
How a tenant-resolution filter works in Spring
multi-tenancy
servlet-filter
thread-local
Intermediate
8 steps
rust
pub fn format_size(bytes: u64) -> String { const UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]; if bytes < 1024 {
Formatting byte counts as human-readable sizes
bit-manipulation
formatting
unit-conversion
Intermediate
5 steps
rust
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; use base64::{DecodeError, Engine}; pub fn encode_standard(data: &[u8]) -> String {
Base64 encode and decode in Rust
base64
encoding
error-handling
Beginner
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/multi-tenant-db-routing-in-axum-extractors-explained-rust-373c/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.