Request Locals
Request locals are values attached to one HTTP request. Middleware can write them to ctx.locals, and request-time Pages, layouts, and API handlers can read them. Ecopages does not persist them between requests.
Declare the shape
Augment the RequestLocals interface in a TypeScript declaration file such as src/modules.d.ts:
import '@ecopages/core';
declare module '@ecopages/core' {
interface RequestLocals {
session?: {
user: { id: string; email: string; role: 'admin' | 'user' };
} | null;
requestId?: string;
theme?: 'light' | 'dark';
}
}The fields are optional unless you declare them otherwise. Keep fields optional when middleware may not set them for every route.
Set locals in middleware
Use Page middleware to authenticate the request, set locals, and stop the request when authentication fails:
import type { FileRouteMiddleware } from '@ecopages/core';
export const authMiddleware: FileRouteMiddleware = async (ctx, next) => {
const session = await authService.validateRequest(ctx.request);
if (!session) {
return Response.redirect(new URL('/login', ctx.request.url));
}
ctx.locals.session = session;
ctx.locals.requestId = crypto.randomUUID();
return next();
};Page middleware runs on every request, so the Page must use cache: 'dynamic':
import { eco } from '@ecopages/core';
import { authMiddleware } from '@/middleware/auth';
export default eco.page({
cache: 'dynamic',
middleware: [authMiddleware],
requires: ['session'],
render: ({ locals }) => <Dashboard user={locals.session.user} />,
});Type requirements
A Page's requires option narrows the named locals in its render callback. For requires: ['session'], TypeScript treats locals.session as present and non-null.
requires is a type-only contract. Ecopages does not check at runtime that middleware populated those locals. Make the middleware enforce the requirement, and do not use requires as an authentication or authorization check. See TypeScript Types for the public type definitions.
API handlers also receive ctx.locals. Their ctx.require('session', onMissing) helper checks that a local is present and non-null at runtime, then returns its narrowed value; if it is missing, the helper terminates the request with the Response returned by onMissing.
Without requires, check optional locals before using them:
export default eco.page({
cache: 'dynamic',
render: ({ locals }) => {
if (!locals?.session) {
return <GuestView />;
}
return <UserView user={locals.session.user} />;
},
});Layouts
Layouts receive optional locals through LayoutProps. They cannot declare requires, so handle absent values:
import { eco } from '@ecopages/core';
import type { JsxRenderable } from '@ecopages/jsx';
export const BaseLayout = eco.layout<JsxRenderable>({
render: ({ children, locals }) => (
<html>
<body>
<Header user={locals?.session?.user} />
<main>{children}</main>
<footer>Request ID: {locals?.requestId}</footer>
</body>
</html>
),
});Cache behavior
Request locals are available only during request-time rendering for Pages with cache: 'dynamic'. A static Page or a Page with a revalidation cache cannot use request locals. Reading locals while such a Page renders throws LocalsAccessError. Page middleware also requires cache: 'dynamic'.
| Page cache | Request locals |
|---|---|
dynamic | Available during each request |
static | Unavailable; access throws LocalsAccessError |
{ revalidate: seconds } | Unavailable; access throws LocalsAccessError |
Semantic error-page renders bypass the normal Page middleware pipeline and receive an empty pageLocals object. Do not rely on Page middleware or requires to provide locals there. See Routing for error-page behavior.