Docs/Runtime & Infrastructure/Middleware

Runtime & Infrastructure

Middleware

Middleware — Runtime & Infrastructure documentation for BCP Framework.

Last updated: 2026-08-29T01:11:41

Middleware

BCP Middleware runs before application routing and can inspect requests, rewrite URLs, redirect requests, change request headers, return responses directly, and add response headers.

BCP 0.1.18 adds Middleware System v2 while keeping the v1 API backward-compatible.

Create middleware

Create middleware.ts in the project root.

text
my-app/
├─ app/
├─ middleware.ts
├─ bcp.config.ts
└─ package.json

Import middleware APIs from bcp/middleware.

Middleware System v2

Middleware v2 uses an onion-style pipeline. A handler can run code before the next handler/application, call await next(), then inspect or change the final Response on the way back out.

TypeScript
import type {
    MiddlewarePipelineHandler,
} from "bcp/middleware";

export const middleware:
    MiddlewarePipelineHandler =
    async (
        request,
        next,
        context
    ) => {
        const started =
            performance.now();

        context.state.requestPath =
            request.nextUrl.pathname;

        const response =
            await next();

        response.headers.set(
            "X-Response-Time",
            `${Math.round(
                performance.now() -
                started
            )}ms`
        );

        return response;
    };

await next() executes the rest of the middleware chain and then the BCP application. The returned value is the real downstream Web Response.

This makes response interception possible without creating an extra API abstraction.

Middleware chains

Export an array to compose several middleware handlers.

TypeScript
import type {
    MiddlewarePipelineHandler,
} from "bcp/middleware";

const logger:
    MiddlewarePipelineHandler =
    async (
        request,
        next
    ) => {
        console.log(
            "request",
            request.method,
            request.nextUrl.pathname
        );

        const response =
            await next();

        console.log(
            "response",
            response.status
        );

        return response;
    };

const responseHeaders:
    MiddlewarePipelineHandler =
    async (
        _request,
        next
    ) => {
        const response =
            await next();

        response.headers.set(
            "X-App",
            "BCP"
        );

        return response;
    };

export const middleware = [
    logger,
    responseHeaders,
];

Execution order is:

text
Request
  ↓
logger before
  ↓
responseHeaders before
  ↓
BCP router / page / API
  ↑
responseHeaders after
  ↑
logger after
  ↓
Response

A middleware handler may call next() only once. Calling it more than once throws an error because a request body and downstream application execution cannot safely be replayed automatically.

Shared request state

The third argument is a MiddlewareContext shared by every handler in one request.

TypeScript
import type {
    MiddlewarePipelineHandler,
} from "bcp/middleware";

const identify:
    MiddlewarePipelineHandler =
    async (
        request,
        next,
        context
    ) => {
        context.state.requestSource =
            request.headers.get(
                "X-Request-Source"
            ) ??
            "web";

        return next();
    };

const observe:
    MiddlewarePipelineHandler =
    async (
        _request,
        next,
        context
    ) => {
        console.log(
            context.state.requestSource
        );

        return next();
    };

export const middleware = [
    identify,
    observe,
];

context.state starts empty for every request and is not shared across concurrent requests.

Use it for lightweight request-scoped information such as timing metadata, correlation IDs, feature flags, or values computed by an earlier middleware handler.

Do not treat it as persistent storage.

Matchers

Use config.matcher to limit the routes that execute middleware.

TypeScript
import type {
    MiddlewareConfig,
} from "bcp/middleware";

export const config:
    MiddlewareConfig = {
    matcher: [
        "/dashboard/:path*",
        "/api/private/:path*",
    ],
};

String matchers support:

text
/dashboard
/users/:id
/dashboard/:path*

A JavaScript RegExp is also supported.

Request data

The middleware request contains:

TypeScript
request.url
request.method
request.headers
request.nextUrl
request.cookies

Example:

TypeScript
export async function middleware(
    request,
    next
) {
    const theme =
        request.cookies.get(
            "theme"
        );

    console.log(
        request.method,
        request.nextUrl.pathname,
        theme
    );

    return next();
}

Response interception

Because next() returns the downstream Response, middleware can add or change headers after a page or API route finishes.

TypeScript
export async function middleware(
    request,
    next
) {
    const response =
        await next();

    response.headers.set(
        "X-Route",
        request.nextUrl.pathname
    );

    return response;
}

Middleware may also replace the response completely:

TypeScript
export async function middleware(
    request,
    next
) {
    const response =
        await next();

    if (
        response.status === 404 &&
        request.nextUrl.pathname
            .startsWith("/api/")
    ) {
        return Response.json(
            {
                error:
                    "API endpoint not found",
            },
            {
                status: 404,
            }
        );
    }

    return response;
}

Rewrite

rewrite() keeps the browser URL but changes the route processed by BCP.

TypeScript
import {
    rewrite,
} from "bcp/middleware";

export function middleware(
    request
) {
    if (
        request.nextUrl.pathname ===
        "/old-dashboard"
    ) {
        return rewrite(
            "/dashboard"
        );
    }
}

Rewrites must remain on the same origin.

In a v2 chain, return rewrite() before calling next().

Redirect

TypeScript
import {
    redirect,
} from "bcp/middleware";

export function middleware(
    request
) {
    if (
        !request.cookies.has(
            "session"
        )
    ) {
        return redirect(
            "/login",
            {
                status: 303,
            }
        );
    }
}

Supported redirect statuses are 301, 302, 303, 307, and 308.

Middleware redirects continue to work with direct document requests and BCP SPA navigation.

Request and response headers

The existing v1 helpers continue to work inside a v2 chain.

TypeScript
import {
    next,
} from "bcp/middleware";

export function middleware() {
    return next({
        request: {
            headers: {
                "X-Internal-Request":
                    "true",
            },
        },
        response: {
            headers: {
                "X-Powered-By-App":
                    "BCP",
            },
        },
    });
}

Request headers are visible to later middleware and the application. Response headers are applied after downstream execution.

Direct responses

A middleware handler can stop the chain by returning a Web Response without calling next().

TypeScript
export function middleware(
    request
) {
    if (
        request.nextUrl.pathname ===
        "/maintenance"
    ) {
        return new Response(
            "Maintenance",
            {
                status: 503,
            }
        );
    }
}

Error middleware pattern

A v2 handler can wrap downstream execution with try/catch.

TypeScript
export async function middleware(
    request,
    next
) {
    try {
        return await next();
    } catch (error) {
        console.error(
            "Request failed",
            request.nextUrl.pathname,
            error
        );

        return Response.json(
            {
                error:
                    "Internal Server Error",
            },
            {
                status: 500,
            }
        );
    }
}

Use this for application-specific error translation. Framework security and server error boundaries still have their own responsibilities.

v1 compatibility

BCP 0.1.18 does not require existing middleware to be rewritten.

A v1 handler remains valid:

TypeScript
import {
    next,
    redirect,
} from "bcp/middleware";

export function middleware(
    request
) {
    if (
        request.nextUrl.pathname ===
        "/private" &&
        !request.cookies.has(
            "session"
        )
    ) {
        return redirect(
            "/login"
        );
    }

    return next();
}

The important difference is:

Capabilityv1v2
Inspect requestYesYes
Match routesYesYes
RedirectYesYes
RewriteYesYes
Request/response header helpersYesYes
Return direct ResponseYesYes
Multiple middleware handlersNoYes
await next() executes downstreamNoYes
Inspect final route responseNoYes
Shared request contextNoYes
Before/after onion flowNoYes

Existing v1 middleware can be migrated gradually. A project may mix legacy one-argument handlers and v2 handlers in the same exported array.

Execution model

At runtime, middleware sits in front of the BCP application.

text
Browser
  ↓
BCP outer gateways
  ↓
Middleware v2 pipeline
  ↓
Actions / Guards / Loaders / Pages / APIs
  ↑
Middleware v2 response path
  ↓
Browser

Development and standalone production use the same middleware pipeline semantics.

Performance note

Middleware response interception crosses a Web Response boundary so BCP can expose the completed downstream response to user middleware. Avoid unnecessary body replacement for large responses when only a header change is needed.

Security constraints

Rewrites must stay on the same origin. Redirects and rewrites reject unsupported URL protocols and embedded URL credentials.

Framework-internal /_bcp/* requests bypass user middleware where required by the runtime. BCP SPA navigation still evaluates middleware against the intended destination URL rather than exposing internal navigation transport details to application policy.

Do not use middleware as a replacement for route authorization when authorization is page-specific. Use route guards and bcp/auth helpers such as requireAuth() or requireRole() for protected application areas.