Middlewares
Define global middlewares that run on every request, or route-level middlewares for fine-grained control over pages and APIs.
Global Middleware
Global middlewares run on every request (both SSR and SPA navigation) before route processing. They establish application context (ctx.locals) that is consistently available throughout your application.
File: global.middleware.ts in the project root
import type { GlobalMiddleware } from "@lolyjs/core";
export const globalMiddlewares: GlobalMiddleware[] = [
async (ctx, next) => {
// Establish context (e.g., session, user, tenant, locale)
// This runs on EVERY request (SSR and SPA navigation)
const sessionCookie = ctx.req.cookies?.session;
if (sessionCookie) {
ctx.locals.user = await getSession(sessionCookie);
ctx.locals.session = { id: sessionCookie };
} else {
ctx.locals.user = null;
ctx.locals.session = null;
}
await next();
},
];Important: Global middlewares should only establish context (configure ctx.locals), not make routing decisions. Routing decisions should be made in layout/page hooks.
Use cases:
- User session/authentication context
- Tenant/multi-tenancy context
- Locale/language detection
- Feature flags based on user
- Request tracing/logging
// global.middleware.ts - Establishes user context
export const globalMiddlewares: GlobalMiddleware[] = [
async (ctx, next) => {
ctx.locals.user = await getSession(ctx.req);
await next();
},
];
// app/dashboard/layout.server.hook.ts - Makes routing decision
export const getServerSideProps: ServerLoader = async (ctx) => {
// ctx.locals.user is already available from global middleware
if (!ctx.locals.user) {
return ctx.Redirect("/login", false);
}
return { props: { user: ctx.locals.user } };
};Benefits:
- Consistent context in SSR and SPA navigation
- No need to repeat session logic in every layout
- Works correctly with client-side navigation
Context Sharing: Global middleware shares ctx.locals with page.server.hook.ts and layout.server.hook.ts. Important: App routes (pages) and API routes use completely separate contexts that do not share data. API routes have their own middleware system (beforeApi) and their own ctx.locals object. If you need shared logic between global middleware and API routes, extract it into a reusable function that both can call.
Page Middlewares
Define middlewares that run before your page loaders for route-specific logic:
Note: For user session/authentication context that should be available everywhere, use global.middleware.ts instead. Route middlewares are best for route-specific validations, permissions, or transformations.
import type { RouteMiddleware, ServerLoader } from "@lolyjs/core";
export const beforeServerData: RouteMiddleware[] = [
async (ctx, next) => {
// Route-specific authorization check
// ctx.locals.user is already available from global.middleware.ts
const user = ctx.locals.user;
if (!user || user.role !== "admin") {
ctx.res.status(403).json({ error: "Forbidden" });
return;
}
await next();
},
];
export const getServerSideProps: ServerLoader = async (ctx) => {
// User is already in ctx.locals from global.middleware.ts
return {
props: { user: ctx.locals.user },
metadata: {
title: "Admin Dashboard",
},
};
};API Middlewares
Define middlewares for API routes:
import type { ApiMiddleware, ApiContext } from "@lolyjs/core";
// Global middleware for all methods
export const beforeApi: ApiMiddleware[] = [
async (ctx, next) => {
// Authentication
const user = await verifyUser(ctx.req);
if (!user) {
return ctx.Response({ error: "Unauthorized" }, 401);
}
ctx.locals.user = user;
await next();
},
];
export async function GET(ctx: ApiContext) {
const user = ctx.locals.user; // Available from middleware
return ctx.Response({ user });
}Method-Specific Middlewares
Define middlewares that only run for specific HTTP methods:
import type { ApiMiddleware, ApiContext } from "@lolyjs/core";
// Middleware only for POST requests
export const beforePOST: ApiMiddleware[] = [
async (ctx, next) => {
// Validation specific to POST
if (!ctx.req.body.title) {
return ctx.Response({ error: "Title required" }, 400);
}
await next();
},
];
// Middleware only for DELETE requests
export const beforeDELETE: ApiMiddleware[] = [
async (ctx, next) => {
// Check permissions for deletion
const user = ctx.locals.user;
if (!user.isAdmin) {
return ctx.Response({ error: "Forbidden" }, 403);
}
await next();
},
];
export async function GET(ctx: ApiContext) {
// No middleware runs for GET
return ctx.Response({ posts: [] });
}
export async function POST(ctx: ApiContext) {
// beforePOST middleware runs first
const post = await createPost(ctx.req.body);
return ctx.Response({ post }, 201);
}
export async function DELETE(ctx: ApiContext) {
// beforeDELETE middleware runs first
await deletePost(ctx.params.id);
return ctx.Response({ deleted: true }, 204);
}Sharing Data with ctx.locals
Use ctx.locals to share data between middlewares and handlers:
// Middleware sets data
export const beforeApi: ApiMiddleware[] = [
async (ctx, next) => {
ctx.locals.user = await getUser(ctx.req);
ctx.locals.requestId = generateId();
await next();
},
];
// Handler accesses data
export async function GET(ctx: ApiContext) {
const { user, requestId } = ctx.locals;
return ctx.Response({ user, requestId });
}