Add middleware
Goal: turn on one of Loco’s 13 built-in middlewares, or write your own when none of them fit, and confirm it’s actually running.
This assumes a working app. For the full config-key/knob table for every built-in middleware, see the Middleware catalog reference.
1. Enable a built-in middleware via config
Every middleware lives under server.middlewares.<key> in your environment YAML (config/development.yaml, config/production.yaml, …). Most are disabled by default; a few (catch_panic, etag, logger, request_id, and fallback outside Production) are enabled unless you write the key at all.
Enable remote_ip (useful behind a proxy/load balancer) and compression:
server: middlewares: remote_ip: enable: true compression: enable: trueWatch out: for middlewares that are enabled by default (e.g.
etag,catch_panic), writing the key at all — even as{}— replaces the framework’s own default with the struct’s own#[serde(default)], which resolvesenabletofalseunless you setenable: trueexplicitly. Don’t add a middleware’s key to config unless you also intend to setenable.
2. Verify it’s registered
cargo loco middleware --configlimit_payload {"body_limit":{"Limit":2000000}}cors (disabled)catch_panic {"enable":true}etag {"enable":true}remote_ip {"enable":true,"source":"RightmostXForwardedFor"}compression {"enable":true}timeout_request (disabled)static (disabled)secure_headers (disabled)logger {"config":{"enable":true},"environment":"development"}request_id {"enable":true}fallback {"enable":true,"code":200,"file":null,"not_found":null}powered_by {"ident":"loco.rs"}cargo loco middleware (without --config) prints just the enabled/disabled state.
3. Common examples
Set a request body size limit:
server: middlewares: limit_payload: body_limit: 5mb # or "disable" to remove the limit entirelyTurn on CORS (disabled by default — note the field is expose_headers, plural):
server: middlewares: cors: enable: true allow_origins: - https://example.com allow_headers: - Content-Type allow_methods: - GET - POST expose_headers: - X-Custom-Header max_age: 3600Serve static assets or an SPA — see Serve static & SPA assets for the full walkthrough:
server: middlewares: static: enable: true folder: uri: "/static" path: "assets/static"Then use the extractor for a middleware that exposes one, e.g. RemoteIP:
use loco_rs::prelude::*;
#[debug_handler]pub async fn list(ip: RemoteIP, State(ctx): State<AppContext>) -> Result<Response> { tracing::info!(%ip, "handling request"); format::json(Entity::find().all(&ctx.db).await?)}4. Apply a middleware to a single route instead of globally
Config-driven middleware always applies to every route in the app. To scope a tower::Layer to one controller or route, use Routes::layer — see Add a controller § 7.
5. Write a custom middleware
Implement the MiddlewareLayer trait:
pub trait MiddlewareLayer { fn name(&self) -> &'static str; fn is_enabled(&self) -> bool { true } // default fn config(&self) -> serde_json::Result<serde_json::Value>; fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>>;}A minimal example that stamps every response with a custom header, config-toggleable like the built-ins:
use axum::{http::HeaderValue, response::Response, Router as AXRouter};use loco_rs::{app::AppContext, controller::middleware::MiddlewareLayer, Result};use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]pub struct HelloHeader { #[serde(default)] pub enable: bool,}
impl MiddlewareLayer for HelloHeader { fn name(&self) -> &'static str { "hello_header" }
fn is_enabled(&self) -> bool { self.enable }
fn config(&self) -> serde_json::Result<serde_json::Value> { serde_json::to_value(self) }
fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> { Ok(app.layer(axum::middleware::map_response(add_header))) }}
async fn add_header(mut res: Response) -> Response { res.headers_mut() .insert("X-Hello", HeaderValue::from_static("loco")); res}Register it alongside (or instead of) the default stack by overriding the middlewares hook on App in src/app.rs:
impl Hooks for App { // ... fn middlewares(ctx: &AppContext) -> Vec<Box<dyn MiddlewareLayer>> { let mut mids = middleware::default_middleware_stack(ctx); mids.push(Box::new(middlewares::hello::HelloHeader { enable: true })); mids }}Remember the ordering rule: AppRoutes::to_router applies this Vec one .layer(...) call at a time, and each new layer wraps the router as the outer layer — so the middleware last in the vec is the first to see an incoming request (LIFO). Push your custom middleware onto whichever end of the vec matches where it needs to sit relative to logger/catch_panic/etc. See the Middleware catalog § stack ordering for the full explanation and the built-in coding order.
Verify
cargo loco middleware --config # confirm your custom entry and its config appearcurl -i localhost:5150/ # confirm the custom header/behavior shows upNext
- Middleware catalog reference — every built-in middleware’s config key and knobs
- Serve static & SPA assets
- Handle errors