How to Implement Custom Middleware and Request Handlers in Topcoat

Topcoat adopts a functional approach where async functions accepting &Cx serve as request handlers, while true middleware concerns leverage the Tower ecosystem through TowerLayer and TowerRoute wrappers.

The tokio-rs/topcoat framework eschews traditional middleware pipelines in favor of composable functions that operate on a request-scoped context. When you need cross-cutting concerns like logging, timeouts, or rate limiting, you implement custom middleware by integrating with Tower, allowing you to wrap routes or mount entire external services as documented in crates/topcoat-router/docs/tower.md.

Understanding Topcoat's Functional Handler Model

Topcoat's architecture rejects hidden middleware chains in favor of explicit function signatures. According to crates/topcoat/docs/functions_not_middlewares.md, you write small, composable functions that receive a request-scoped &Cx and return a value implementing IntoResponse.

Writing Request Handlers with Cx

A request handler is any async function that takes cx: &Cx (or mut cx: Cx when mutation is required) and returns a type implementing IntoResponse. The Cx context, documented in crates/topcoat/docs/context.md, provides rich extraction helpers for query parameters, path parameters, and body parsers.

use topcoat::prelude::*;

/// Returns a greeting. The function receives the request context `cx`.
#[page]
async fn hello(cx: &Cx) -> Result<impl IntoResponse> {
    // Extract a query param `name` (or default to "world").
    let name: Option<String> = cx.query().get("name").await?;
    let greeting = format!("Hello, {}", name.unwrap_or_else(|| "world".into()));
    Ok(greeting)
}

The handler uses cx.query().get() to extract optional parameters, demonstrating how request extraction works without explicit middleware.

Registering Handlers via Macros and Builder API

You can register handlers using attribute macros or the builder API defined in crates/topcoat-router/src/lib.rs. The #[page] macro automatically registers the function with the router. Alternatively, use Router::builder():

let router = Router::builder()
    .route(MyHandler::new())
    .build();

For route-specific handlers, use #[route] or the route() method with the handler struct.

Implementing Custom Middleware with Tower

When you need true middleware for concerns like timeouts, compression, or CORS, Topcoat leverages the Tower ecosystem. The implementation in crates/topcoat-router/src/tower.rs provides thin wrappers that adapt Tower layers for Topcoat's router.

Creating a Custom Tower Layer

Implement tower::Layer to create reusable middleware that sees the full http::Request. This layer can modify requests, short-circuit responses, or add headers before the Topcoat handler runs.

use std::{task::{Context, Poll}, pin::Pin, future::Future};
use tower::{Layer, Service};
use topcoat::router::{Router, tower::TowerLayer};

/// Simple logging layer that prints the request URI.
#[derive(Clone)]
struct LoggingLayer;

impl<S> Layer<S> for LoggingLayer {
    type Service = LoggingService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        LoggingService { inner }
    }
}

#[derive(Clone)]
struct LoggingService<S> {
    inner: S,
}

impl<S, ReqBody> Service<http::Request<ReqBody>> for LoggingService<S>
where
    S: Service<http::Request<ReqBody>, Response = http::Response<ReqBody>> + Clone + Send + 'static,
    S::Future: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: http::Request<ReqBody>) -> Self::Future {
        println!("Incoming request: {}", req.uri());
        let fut = self.inner.call(req);
        Box::pin(async move { fut.await })
    }
}

Attach the layer to your router using TowerLayer::new().

Scoping Middleware with TowerLayer::at

By default, layers apply to every route. Scope them to specific paths using TowerLayer::at() as shown in crates/topcoat-router/docs/tower.md:

// Apply the middleware only to the `/api` namespace.
let router = Router::builder()
    .layer(TowerLayer::new(LoggingLayer).at("/api"))
    .build();

The at("/api") method restricts the middleware to requests matching that prefix, allowing fine-grained control over where middleware executes.

Using Pre-built Tower Middleware

Reuse existing Tower layers like TimeoutLayer without custom implementation:

use std::time::Duration;
use topcoat::router::{Router, tower::TowerLayer};
use tower::timeout::TimeoutLayer;

let router = Router::builder()
    .layer(
        // Abort any request that takes longer than 5 seconds.
        TowerLayer::new(TimeoutLayer::new(Duration::from_secs(5))).at("/api")
    )
    .build();

This pattern applies standard Tower middleware to Topcoat routes while maintaining the functional handler model for your application logic.

Mounting External Services with TowerRoute

If you have existing Axum routers, Hyper services, or any tower::Service, expose them as subtrees using TowerRoute. This facilitates incremental migration of legacy code into Topcoat while keeping the original service intact, as implemented in crates/topcoat-router/src/tower.rs.

use topcoat::router::{Router, Methods, tower::TowerRoute};
use axum::Router as AxumRouter;

// Existing Axum app that serves legacy endpoints.
let legacy_axum: AxumRouter = legacy_app();

// Expose the whole Axum tree under `/legacy`.
let router = Router::builder()
    .route(TowerRoute::new(Methods::Any, "/legacy/{*rest}", legacy_axum))
    .build();

The TowerRoute::new() method takes an HTTP method (or Methods::Any), a path pattern including wildcards like {*rest}, and the service to mount. This integrates external services at the router level, distinct from function-based handlers.

Summary

  • Write handlers as functions: Create async functions taking &Cx that return IntoResponse, leveraging extraction methods on the context object defined in crates/topcoat/docs/context.md.
  • Implement Tower layers: For cross-cutting concerns, implement tower::Layer and Service to process raw http::Request objects before they reach your handlers.
  • Apply middleware selectively: Use TowerLayer::new(your_layer).at("/prefix") to scope middleware to specific route prefixes, as documented in crates/topcoat-router/docs/tower.md.
  • Mount legacy services: Use TowerRoute::new() to integrate existing Axum or Tower services under specific paths, enabling incremental migration strategies.

Frequently Asked Questions

What is the difference between a handler and middleware in Topcoat?

Handlers are async functions that accept &Cx and return IntoResponse, focusing on generating responses for specific routes. Middleware, implemented via Tower layers, intercepts the raw http::Request before it reaches the handler and can modify the request or response. Topcoat reserves middleware for concerns like timeouts and logging, while business logic lives in functional handlers, according to crates/topcoat/docs/functions_not_middlewares.md.

How do I access the raw HTTP request in a handler?

Access specific parts of the request through the Cx context extraction helpers (e.g., cx.query(), cx.body_json()). If you need the raw http::Request, implement a custom Tower layer that processes the request before passing it to the handler, as the Cx abstraction intentionally hides raw request details to encourage type-safe extraction.

Can I use existing Axum middleware with Topcoat?

Yes. Since Axum is built on Tower, any Axum middleware that implements tower::Layer works with Topcoat via TowerLayer::new(). You can also mount an entire Axum router as a subtree using TowerRoute::new(Methods::Any, "/path/{*rest}", axum_router), allowing you to reuse existing Axum ecosystems within a Topcoat application.

Where does the Cx context type come from?

The Cx type is defined in the topcoat core and documented in crates/topcoat/docs/context.md. It provides request-scoped data extraction, body parsing, and response building capabilities. The context is passed by reference (&Cx) to handlers, with mutable access (mut cx: Cx) available when you need to consume the request body or modify response headers directly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →