rust
75 lines · 8 steps
Substate extraction with FromRef in Axum
How Axum's FromRef lets handlers pull just the piece of app state they need instead of the whole struct.
Explained by
highlit
1use std::sync::Arc;
2
3use axum::{
4 extract::{FromRef, State},
5 http::StatusCode,
6 routing::{get, post},
7 Json, Router,
8};
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone)]
12struct AppState {
13 db: Arc<PgPool>,
14 auth: AuthState,
15}
16
17#[derive(Clone)]
18struct AuthState {
19 jwt_secret: Arc<str>,
20 sessions: Arc<SessionStore>,
21}
22
23impl FromRef<AppState> for AuthState {
24 fn from_ref(state: &AppState) -> Self {
25 state.auth.clone()
26 }
27}
28
29impl FromRef<AppState> for Arc<PgPool> {
30 fn from_ref(state: &AppState) -> Self {
31 state.db.clone()
32 }
33}
34
35#[derive(Deserialize)]
36struct LoginRequest {
37 email: String,
38 password: String,
39}
40
41#[derive(Serialize)]
42struct TokenResponse {
43 token: String,
44}
45
46fn public_routes() -> Router<AppState> {
47 Router::new()
48 .route("/health", get(|| async { StatusCode::OK }))
49 .route("/auth/login", post(login))
50}
51
52fn authenticated_routes() -> Router<AppState> {
53 Router::new()
54 .route("/me", get(current_user))
55 .route("/posts", post(create_post))
56 .layer(axum::middleware::from_fn(require_bearer_token))
57}
58
59pub fn app(state: AppState) -> Router {
60 Router::new()
61 .nest("/api", public_routes().merge(authenticated_routes()))
62 .with_state(state)
63}
64
65async fn login(
66 State(auth): State<AuthState>,
67 State(db): State<Arc<PgPool>>,
68 Json(body): Json<LoginRequest>,
69) -> Result<Json<TokenResponse>, StatusCode> {
70 let user = User::verify_credentials(&db, &body.email, &body.password)
71 .await
72 .map_err(|_| StatusCode::UNAUTHORIZED)?;
73 let token = auth.issue_token(user.id);
74 Ok(Json(TokenResponse { token }))
75}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1FromRef decouples handlers from the full state struct so each one asks only for the substate it uses.
- 2Wrapping shared resources in Arc makes cloning state per-request cheap regardless of what it holds.
- 3Splitting routes into groups lets you attach middleware like auth to exactly the endpoints that need it.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 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
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
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/substate-extraction-with-fromref-in-axum-explained-rust-0ec7/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.