Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

composeServerMiddleware

Composes a list of server middleware around a Fetch handler, from outside in, as Meeko does with server/middleware files.

server Since 1.0.0 @meekojs/meeko/server Source code
ts
import { composeServerMiddleware } from "@meekojs/meeko/server";

composeServerMiddleware(middlewares, handler) chains (request, next) => Response functions around a final handler and returns a (request) => Promise<Response> handler. This is what createServerApp does with server/middleware/ files (by filename) and middleware added by modules: each middleware can continue with next(), change the request or response, or short-circuit by returning its own Response.

Composition is strict: next() can only be called once, each middleware must return a Response and, if middleware forgets to await next(), the downstream chain is still awaited and its errors are not lost.

Usage#

ts
import { composeServerMiddleware } from "@meekojs/meeko/server";
import type { ServerMiddleware } from "@meekojs/meeko/server";

const timing: ServerMiddleware = async (request, next) => {
  const start = performance.now();
  const response = await next();
  console.log(request.method, new URL(request.url).pathname, response.status, `${Math.round(performance.now() - start)} ms`);
  return response;
};

const handle = composeServerMiddleware([timing], async () => new Response("hello"));
const response = await handle(new Request("http://localhost/"));

Type#

ts
function composeServerMiddleware(
  middlewares: readonly ServerMiddleware[],
  handler: (request: Request) => Promise<Response>,
): (request: Request) => Promise<Response>

ServerMiddleware

ts
type ServerMiddleware = (
  request: Request,
  next: (request?: Request) => Promise<Response>,
) => Response | Promise<Response>

Parameters#

Parameter Type Description
middlewares readonly ServerMiddleware[] In execution order: the first is outermost.
handler (request: Request) => Promise<Response> The final handler, such as app.fetch.

A ServerMiddleware is (request, next) => Response | Promise<Response>; next(request?) continues with the same request or a different one.

Return value#

A (request: Request) => Promise<Response> handler. Rejects if middleware or the handler throws; if both fail, it rejects with an AggregateError (Server middleware and downstream failed) preserving both errors.

Examples#

In server/middleware/, without manual composition#

ts
// server/middleware/20-maintenance.ts
import { defineServerMiddleware } from "@meekojs/meeko/server";

export default defineServerMiddleware(async (request, next) => {
  if (process.env.MAINTENANCE === "1" && !new URL(request.url).pathname.startsWith("/_meeko/")) {
    return new Response("En mantenimiento", { status: 503, headers: { "retry-after": "120" } });
  }
  return next();
});

Meeko sorts files by name (10-…, 20-…) and composes them with this same function, around pages, /api and /_meeko/rpc.

An access log with the real IP (since 1.7)#

ts
// server/middleware/10-access-log.ts
import { clientAddress, defineServerMiddleware } from "@meekojs/meeko/server";

export default defineServerMiddleware(async (request, next) => {
  const ip = clientAddress(request) ?? "unknown"; // applies MEEKO_TRUST_PROXY
  const response = await next();
  console.log(`${request.method} ${new URL(request.url).pathname} ${response.status} ${ip}`);
  return response;
});

Change the downstream request#

ts
import type { ServerMiddleware } from "@meekojs/meeko/server";

export const tagRequests: ServerMiddleware = (request, next) => {
  const headers = new Headers(request.headers);
  headers.set("x-request-start", String(Date.now()));
  return next(new Request(request, { headers })); // preserves the connection IP
};

A custom host wrapping the application#

ts
import { createServer } from "node:http";
import { composeServerMiddleware } from "@meekojs/meeko/server";
import { toNodeListener } from "@meekojs/meeko/node";

const guarded = composeServerMiddleware([timing, tagRequests], (request) => app.fetch(request));
createServer(toNodeListener(guarded, { publicOrigin: "https://example.com" })).listen(3000);

Common mistakes#

Call next() twice#

Incorrectts
const retry: ServerMiddleware = async (request, next) => {
  const response = await next();
  return response.status >= 500 ? next() : response;
};

The second call throws Error: Server middleware called next() more than once and the request responds with 500. The request body has already been consumed: a retry needs a new Request and another composition.

Correctts
const retry: ServerMiddleware = async (request, next) => next(request);

Do not return the response#

Incorrectts
const log: ServerMiddleware = async (request, next) => {
  const response = await next();
  console.log(response.status);
};

TypeError: Server middleware must return a Response at runtime; meeko check already marks it as a type error.

Correctts
const log: ServerMiddleware = async (request, next) => {
  const response = await next();
  console.log(response.status);
  return response;
};

Read X-Forwarded-For manually#

Incorrectts
const ip = request.headers.get("x-forwarded-for")?.split(",")[0];

Any client can send this header: the IP can be spoofed. clientAddress(request) only reads it with MEEKO_TRUST_PROXY and takes the trusted proxy's entry.

Correctts
import { clientAddress } from "@meekojs/meeko/server";
export const ipOf = (request: Request) => clientAddress(request);

Notes#

  • Before context. Server middleware runs before creating the session and RequestContext; API and RPC guards (Origin, CSRF, auth) remain inside the handler.
  • What it wraps. Pages (including prerendered ones), /api and /_meeko/rpc. Public build files are served first, and Socket upgrades have their own handler.
  • Immutable responses. A Response from fetch or Response.redirect has immutable headers: create a new one with new Response(response.body, response) before changing them.
  • IP. A next(new Request(request, …)) preserves the connection's IP, so rateLimit and clientAddress still count by client.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/server/composeservermiddleware.md