Skip to content

Middleware ​

Middleware in Cerces allows you to run code on every request and response, enabling cross-cutting concerns like logging, authentication, CORS handling, compression, and more.

Middleware functions act as interceptors that can:

  • Inspect requests before they reach your route handlers
  • Process responses before they're sent to clients
  • Handle errors and edge cases consistently across all routes
  • Add common functionality without duplicating code in individual routes

This is perfect for tasks like request logging, setting response headers, integrity checks, rate limiting, and response compression.

Creating Middleware ​

Create middleware using the Middleware class. The handle function receives two parameters:

  • Context object: Contains req (Request), along with other runtime arguments.
  • next function: Calls the next middleware or route handler in the chain.
ts
import { Middleware } from "cerces"

const requestLogger = new Middleware({
    name: "requestLogger", // optional name for debugging
    handle: async ({ req }, next) => {
        console.log(`${req.method} ${req.url}`)
        const start = Date.now()

        const response = await next() // call next middleware/route handler

        const duration = Date.now() - start
        console.log(`Response: ${response.status} (${duration}ms)`)

        return response
    },
})

Apply middleware to your app:

ts
import { App } from "cerces"

const app = new App({
    middleware: [requestLogger] 
})

Middleware Execution Order

Middleware executes in the order you define it. The first middleware in your array runs first on requests and last on responses (like an onion - outer layers first, then inner layers).

Processing Order ​

Middleware can run code before the request reaches route handlers and after responses are generated:

ts
const timingMiddleware = new Middleware({
    handle: async ({ req }, next) => {
        // BEFORE: Request processing
        const startTime = Date.now()
        console.log(`Processing ${req.method} ${req.url}`)

        const response = await next() // Route handler executes here

        // AFTER: Response processing
        const duration = Date.now() - startTime
        response.headers.set("X-Response-Time", `${duration}ms`)
        console.log(`Completed in ${duration}ms`)

        return response
    },
})

Before request processing: Perfect for logging, authentication, request modification, or early validation.

After response processing: Ideal for adding headers, logging response metrics, compression, or cleanup tasks.

Maybe Dependencies? ​

While middleware can intercept and process requests/responses, it cannot provide additional data to route handlers. For that, use dependencies:

Use middleware for:

  • Request/response logging and monitoring
  • Adding headers to responses
  • CORS handling
  • Compression
  • Error handling and recovery

Use dependencies for:

  • Providing processed data to route handlers (user objects, database connections, etc.)
  • Parameter validation and transformation
  • Business logic that handlers need access to
  • Reusable data fetching and processing
ts
// ❌ Middleware cannot provide data to handlers
const authMiddleware = new Middleware({
    handle: async ({ req }, next) => {
        const user = await authenticateUser(req)
        // This user data is NOT available in route handlers!
        return next()
    },
})
ts
// ✅ Dependencies CAN provide data to handlers
const requireAuth = new Dependency({
    parameters: { authorization: Header(z.string()) },
    handle: async ({ authorization }) => {
        const user = await authenticateUser(authorization)
        return user // This IS available in route handlers!
    }
})

app.get("/profile", {
    parameters: {
        user: Depends(requireAuth), // user data available here
    },
    handle: ({ user }) => ({ profile: user.profile })
})

Choose Wisely

If your route handlers need access to processed data (user objects, parsed tokens, database results), use dependencies. Use middleware for cross-cutting concerns that don't need to pass data to handlers.

Runtime Nuances

In some runtimes, Request objects may be immutable. Always check your runtime's documentation for request/response handling limitations.

Scoped Middleware ​

Apply middleware to specific routes or routers instead of globally:

Router-level middleware (affects all routes in that router):

ts
import { Router, Base } from "cerces"
import type app from "src/index"

const apiRouter = new Router({
    base: Base<typeof app>(),
    middleware: [authMiddleware] // applies to all routes in this router
})

apiRouter.get("/users", { /* ... */ })
apiRouter.post("/posts", { /* ... */ })
ts
app.include("/api", apiRouter) // /api/users and /api/posts get auth middleware

Route-level middleware (affects only specific routes):

ts
app.get("/admin", {
    middleware: [adminAuthMiddleware], // only this route
    parameters: {},
    handle: () => ({ message: "Admin area" })
})

Middleware merging: When middleware is defined at multiple levels, they're combined in this order:

  1. App-level middleware
  2. Router-level middleware
  3. Route-level middleware

Each level's middleware wraps the next, maintaining proper execution order.

Built-in Middleware ​

Cerces provides ready-to-use middleware for common web development needs:

  • CORS: Handle Cross-Origin Resource Sharing with flexible configuration.
  • Trusted Host: Restrict requests to specified hostnames for security.
  • Compress: Automatically compress responses to save bandwidth.