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.
my-app/
├─ app/
├─ middleware.ts
├─ bcp.config.ts
└─ package.jsonImport 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.
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.
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:
Request
↓
logger before
↓
responseHeaders before
↓
BCP router / page / API
↑
responseHeaders after
↑
logger after
↓
ResponseA 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.
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.
import type {
MiddlewareConfig,
} from "bcp/middleware";
export const config:
MiddlewareConfig = {
matcher: [
"/dashboard/:path*",
"/api/private/:path*",
],
};String matchers support:
/dashboard
/users/:id
/dashboard/:path*A JavaScript RegExp is also supported.
Request data
The middleware request contains:
request.url
request.method
request.headers
request.nextUrl
request.cookiesExample:
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.
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:
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.
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
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.
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().
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.
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:
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:
| Capability | v1 | v2 |
|---|---|---|
| Inspect request | Yes | Yes |
| Match routes | Yes | Yes |
| Redirect | Yes | Yes |
| Rewrite | Yes | Yes |
| Request/response header helpers | Yes | Yes |
Return direct Response | Yes | Yes |
| Multiple middleware handlers | No | Yes |
await next() executes downstream | No | Yes |
| Inspect final route response | No | Yes |
| Shared request context | No | Yes |
| Before/after onion flow | No | Yes |
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.
Browser
↓
BCP outer gateways
↓
Middleware v2 pipeline
↓
Actions / Guards / Loaders / Pages / APIs
↑
Middleware v2 response path
↓
BrowserDevelopment 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.