Skip to content
loco
v1.0
★ 6.9k Get started

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: true

Watch 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 resolves enable to false unless you set enable: true explicitly. Don’t add a middleware’s key to config unless you also intend to set enable.

2. Verify it’s registered

Terminal window
cargo loco middleware --config
Terminal window
limit_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 entirely

Turn 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: 3600

Serve 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:

src/middlewares/hello.rs
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

Terminal window
cargo loco middleware --config # confirm your custom entry and its config appear
curl -i localhost:5150/ # confirm the custom header/behavior shows up

Next