Middleware
Context accumulation with type-safe middleware
Middleware extends the handler context with new properties. Each .use() call adds to what came before — TypeScript tracks the accumulated context so your handler sees exactly what's available.
Inline Middleware
The simplest middleware is an inline function that returns an object. The returned properties are merged into the context:
import { Get } from '@justscale/http';
Get('/tickets')
.use(() => ({ requestedAt: Date.now() }))
.handle(({ requestedAt, res }) => {
// requestedAt is typed as number — added by the middleware
res.json({ requestedAt });
});Context Accumulation
Multiple .use() calls accumulate context. Each middleware sees everything that came before it:
import { Get } from '@justscale/http';
import { auth } from '@justscale/auth';
Get('/tickets/:ticket')
.use(auth) // ctx now has: { user }
.use(async ({ user }) => { // can access user from previous .use()
const role = user.email.endsWith('@support.com') ? 'agent' : 'customer';
return { role }; // adds: { role }
})
.handle(({ user, role, params, res }) => {
// Both user and role are available and typed
if (role === 'agent') {
res.json({ tickets: 'all' });
} else {
res.json({ tickets: 'own' });
}
});Async Middleware
Middleware can be async — useful for loading data that the handler needs:
import { Get } from '@justscale/http';
import { auth } from '@justscale/auth';
// Async middleware — fetches additional data before the handler runs
Get('/dashboard')
.use(auth)
.use(async ({ user }) => {
// Fetch preferences from an external API
const res = await fetch(`https://api.example.com/prefs/${user.email}`);
const preferences = await res.json();
return { preferences }; // adds { preferences } to context
})
.handle(({ user, preferences, res }) => {
// Both user (from auth) and preferences (from async middleware) available
res.json({ user: user.email, theme: preferences.theme });
});DI Middleware (MiddlewareDef)
For middleware that needs injected services, use createMiddleware. This lets middleware participate in the DI system:
import { createMiddleware, Logger } from '@justscale/core';
// Middleware with DI — Logger is injected automatically
export const auditLog = createMiddleware({
inject: { logger: Logger },
handler: ({ logger }) => async (ctx: { user?: { email: string } }) => {
const who = ctx.user?.email ?? 'anonymous';
logger.info('Request', { user: who });
return { auditedAt: new Date() };
},
});The auth Middleware
The most common middleware is auth from @justscale/auth. It validates the session token and adds user to the context:
import { Get, Post } from '@justscale/http';
import { auth } from '@justscale/auth';
// After .use(auth), the handler has { user: Persistent<User> }
Get('/tickets')
.use(auth)
.handle(({ user, res }) => {
// user.email, user.name — typed fields from the User model
res.json({ loggedInAs: user.email });
});
// Without auth — no user in context
Get('/health').handle(({ res }) => {
res.json({ status: 'ok' });
});The permissions Middleware
Stack permissions from @justscale/permission after auth when a route uses permission-scoped .returns(). It resolves the caller's principals, stores them for downstream query filtering, and wraps res with a .permission discriminant matching the rule that fired.
import { Get } from '@justscale/http';
import { auth } from '@justscale/auth';
import { permissions, assertNever } from '@justscale/permission';
import { Employee } from '../models/employee';
import { EmployeeFull, EmployeeLimited } from '../schemas/employee';
Get('/employees/:employee')
.types({ Employee })
.use(auth)
.use(permissions)
.guard(Employee.can.view)
.returns(200, EmployeeFull, Employee.can.fullAccess)
.returns(200, EmployeeLimited, Employee.can.view)
.handle(({ params, res }) => {
switch (res.permission) {
case 'fullAccess': res.json(params.employee); return;
case 'view': res.json({ name: params.employee.name }); return;
default: assertNever(res);
}
});See Permissions for the full picture — declaring rules on models and querying with them.
Best Practices
- Middleware adds, never removes — each
.use()extends the context. It can't remove properties added by previous middleware - Order matters — later middleware can access earlier additions.
.use(auth).use(loadProfile)works; the reverse doesn't - Use DI middleware for shared logic — if middleware needs services, use
createMiddlewareinstead of closures - Throw to stop — if middleware throws, execution stops and the error is returned to the client