Express & Fastify Adapters
Overview
@authrim/server provides first-class adapters for Express and Fastify. Both adapters validate incoming access tokens (Bearer and DPoP), set authentication context on the request object, and return proper WWW-Authenticate error headers.
Common: MiddlewareOptions
Both adapters accept a shared MiddlewareOptions configuration:
import type { MiddlewareOptions } from '@authrim/server';
const options: MiddlewareOptions = { /** Realm value for WWW-Authenticate header */ realm: 'my-api',
/** Custom error handler — called when token validation fails */ onError: (error) => { console.error('Auth error:', error.code, error.message); },};| Option | Type | Default | Description |
|---|---|---|---|
realm | string | undefined | Realm included in WWW-Authenticate response header |
onError | (error: AuthrimServerError) => void | undefined | Callback invoked when validation fails (for logging, metrics) |
Express Adapter
Import from @authrim/server/adapters/express.
authrimMiddleware()
Required authentication — rejects requests without a valid token (responds with 401):
import express from 'express';import { createAuthrimServer } from '@authrim/server';import { authrimMiddleware } from '@authrim/server/adapters/express';
const server = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com',});
const app = express();
// Protect all routes under /apiapp.use('/api', authrimMiddleware(server));
app.get('/api/profile', (req, res) => { // req.auth is guaranteed to be set (ValidatedToken) res.json({ sub: req.auth.sub, email: req.auth.email, tokenType: req.authTokenType, // 'Bearer' | 'DPoP' });});authrimOptionalMiddleware()
Optional authentication — continues even without a token. req.auth is undefined for unauthenticated requests:
import { authrimOptionalMiddleware } from '@authrim/server/adapters/express';
app.get( '/api/posts', authrimOptionalMiddleware(server), (req, res) => { if (req.auth) { // Authenticated — show personalized content res.json({ posts: getPostsForUser(req.auth.sub), user: req.auth.sub }); } else { // Anonymous — show public content res.json({ posts: getPublicPosts() }); } },);Request Properties
After middleware runs, these properties are available on req:
| Property | Type | Description |
|---|---|---|
req.auth | ValidatedToken | undefined | Validated token claims (sub, iss, aud, exp, etc.) |
req.authTokenType | 'Bearer' | 'DPoP' | undefined | Token type used for authentication |
Scope-Based Access Control
function requireScope(...scopes: string[]) { return (req: express.Request, res: express.Response, next: express.NextFunction) => { const tokenScopes = req.auth?.scope?.split(' ') ?? []; const hasAll = scopes.every((s) => tokenScopes.includes(s));
if (!hasAll) { return res.status(403).json({ error: 'insufficient_scope', error_description: `Required scopes: ${scopes.join(' ')}`, }); } next(); };}
app.get( '/api/admin/users', authrimMiddleware(server), requireScope('admin:read'), (req, res) => { res.json({ users: getAllUsers() }); },);Complete Express Example
import express from 'express';import { createAuthrimServer } from '@authrim/server';import { authrimMiddleware, authrimOptionalMiddleware,} from '@authrim/server/adapters/express';
const server = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com',});
const app = express();app.use(express.json());
// Public endpoint — no authapp.get('/health', (req, res) => { res.json({ status: 'ok' });});
// Optional auth — enhanced for logged-in usersapp.get('/api/feed', authrimOptionalMiddleware(server), (req, res) => { if (req.auth) { res.json({ feed: getPersonalizedFeed(req.auth.sub) }); } else { res.json({ feed: getPublicFeed() }); }});
// Required auth — protected routesapp.use('/api/me', authrimMiddleware(server));
app.get('/api/me/profile', (req, res) => { res.json({ sub: req.auth.sub, email: req.auth.email });});
app.listen(3000, () => console.log('Server running on :3000'));TypeScript Type Extension
To add proper types for req.auth and req.authTokenType, augment the Express namespace:
import type { ValidatedToken } from '@authrim/server';
declare global { namespace Express { interface Request { auth: ValidatedToken; authTokenType: 'Bearer' | 'DPoP'; } }}Fastify Adapter
Import from @authrim/server/adapters/fastify.
Fastify offers two integration styles: per-route handlers and app-wide plugins.
Per-Route: authrimPreHandler / authrimOptionalPreHandler
Attach authentication to specific routes using Fastify’s preHandler hook:
import Fastify from 'fastify';import { createAuthrimServer } from '@authrim/server';import { authrimPreHandler, authrimOptionalPreHandler,} from '@authrim/server/adapters/fastify';
const server = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com',});
const app = Fastify();
// Required auth on a single routeapp.get('/api/profile', { preHandler: authrimPreHandler(server), handler: (request, reply) => { reply.send({ sub: request.auth.sub, tokenType: request.authTokenType, }); },});
// Optional auth on a single routeapp.get('/api/posts', { preHandler: authrimOptionalPreHandler(server), handler: (request, reply) => { if (request.auth) { reply.send({ posts: getPostsForUser(request.auth.sub) }); } else { reply.send({ posts: getPublicPosts() }); } },});App-Wide: authrimPlugin / authrimOptionalPlugin
Register authentication as a Fastify plugin to protect all routes within a scope:
import { authrimPlugin, authrimOptionalPlugin,} from '@authrim/server/adapters/fastify';
const app = Fastify();
// Protect all routes registered after this pluginapp.register(async (instance) => { await instance.register(authrimPlugin(server));
instance.get('/api/profile', (request, reply) => { reply.send({ sub: request.auth.sub }); });
instance.get('/api/settings', (request, reply) => { reply.send({ settings: getUserSettings(request.auth.sub) }); });});
// Routes outside the scope are unaffectedapp.get('/health', (request, reply) => { reply.send({ status: 'ok' });});Request Properties
| Property | Type | Description |
|---|---|---|
request.auth | ValidatedToken | undefined | Validated token claims |
request.authTokenType | 'Bearer' | 'DPoP' | undefined | Token type used |
Complete Fastify Example
import Fastify from 'fastify';import { createAuthrimServer } from '@authrim/server';import { authrimPreHandler, authrimOptionalPreHandler, authrimPlugin,} from '@authrim/server/adapters/fastify';
const server = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com',});
const app = Fastify({ logger: true });
// Health check — no authapp.get('/health', (request, reply) => { reply.send({ status: 'ok' });});
// Optional auth routeapp.get('/api/feed', { preHandler: authrimOptionalPreHandler(server), handler: (request, reply) => { reply.send({ user: request.auth?.sub ?? 'anonymous', feed: request.auth ? getPersonalizedFeed(request.auth.sub) : getPublicFeed(), }); },});
// Protected scope using pluginapp.register(async (instance) => { await instance.register(authrimPlugin(server, { realm: 'my-api', onError: (err) => app.log.warn({ code: err.code }, 'Auth failed'), }));
instance.get('/api/profile', (request, reply) => { reply.send({ sub: request.auth.sub, email: request.auth.email }); });
instance.delete('/api/account', (request, reply) => { deleteAccount(request.auth.sub); reply.code(204).send(); });});
app.listen({ port: 3000 });TypeScript Module Augmentation
import type { ValidatedToken } from '@authrim/server';
declare module 'fastify' { interface FastifyRequest { auth: ValidatedToken; authTokenType: 'Bearer' | 'DPoP'; }}Comparison: Express vs Fastify
| Aspect | Express | Fastify |
|---|---|---|
| Required auth (per-route) | authrimMiddleware(server) | authrimPreHandler(server) |
| Optional auth (per-route) | authrimOptionalMiddleware(server) | authrimOptionalPreHandler(server) |
| App-wide auth | app.use(authrimMiddleware(server)) | authrimPlugin(server) / authrimOptionalPlugin(server) |
| Auth location | req.auth | request.auth |
| Token type location | req.authTokenType | request.authTokenType |
| Type augmentation | declare global { namespace Express } | declare module 'fastify' |
| Error response | 401 with WWW-Authenticate | 401 with WWW-Authenticate |
Next Steps
- Hono, Koa & NestJS Adapters — Additional framework integrations
- Error Handling — Error codes and response utilities
- Configuration Reference — Server configuration options