rust 45 lines · 7 steps

Serving an SPA and API with Axum

A single Axum router combines a JSON API, static assets, and a single-page-app fallback under one tree.

Explained by highlit
1use axum::{
2 http::StatusCode,
3 response::IntoResponse,
4 routing::get,
5 Json, Router,
6};
7use serde::Serialize;
8use std::path::PathBuf;
9use tower_http::services::{ServeDir, ServeFile};
10use tower_http::trace::TraceLayer;
11 
12#[derive(Serialize)]
13struct HealthResponse {
14 status: &'static str,
15 version: &'static str,
16}
17 
18async fn health() -> impl IntoResponse {
19 Json(HealthResponse {
20 status: "ok",
21 version: env!("CARGO_PKG_VERSION"),
22 })
23}
24 
25async fn not_found() -> impl IntoResponse {
26 (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "resource not found" })))
27}
28 
29pub fn build_router() -> Router {
30 let dist = PathBuf::from("frontend/dist");
31 let index = dist.join("index.html");
32 
33 let assets = ServeDir::new(dist.join("assets"));
34 let spa = ServeDir::new(&dist).fallback(ServeFile::new(index));
35 
36 let api = Router::new()
37 .route("/health", get(health))
38 .fallback(not_found);
39 
40 Router::new()
41 .nest("/api", api)
42 .nest_service("/assets", assets)
43 .fallback_service(spa)
44 .layer(TraceLayer::new_for_http())
45}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Nesting lets you compose independent routers so API and static concerns stay cleanly separated.
  2. 2A ServeFile fallback on a ServeDir is the standard trick to make client-side routing work for an SPA.
  3. 3Handlers returning impl IntoResponse can freely mix status codes, JSON, and tuples without boilerplate.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Serving an SPA and API with Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code