Middleware
Global cross-cutting concerns
Middleware allows you to add global, cross-cutting concerns to your API. It runs before procedure handlers and can modify context, validate inputs, or block requests.
Creating Middleware
To create middleware, use createMiddleware and return an error or call opts.next(). The following example demonstrates an authentication middleware that checks for a user in the context:
// src/middleware/auth.ts
import { createMiddleware } from '@deessejs/server'
import { err } from '@deessejs/fp'
import { UnauthorizedException } from '@deessejs/server'
export const authMiddleware = createMiddleware({
name: 'auth',
handler: async (ctx, opts) => {
// Check for auth token in context
if (!ctx.user) {
return err(new UnauthorizedException('Not authenticated'))
}
// Continue to next middleware/handler
return opts.next()
},
})Using Middleware
You can apply middleware globally to all procedures or per-procedure. The example below shows how to apply middleware globally:
// src/api.ts
import { defineContext } from '@deessejs/server'
const { t, createAPI } = defineContext({
context: () => ({ user: null }),
})
export const api = createAPI({
router: appRouter,
middleware: [authMiddleware],
})Apply middleware per-procedure using .use():
import { defineContext, t } from '@deessejs/server'
import { z } from 'zod'
import { ok } from '@deessejs/fp'
const publicQuery = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
return ok(await ctx.db.posts.find(args.id))
},
})
const privateQuery = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
return ok(await ctx.db.posts.find(args.id))
},
}).use(requireAdmin)Middleware with Arguments
To pass arguments to middleware, return a function from createMiddleware. This lets you create reusable middleware factories:
// src/middleware/requireRole.ts
import { createMiddleware } from '@deessejs/server'
import { err } from '@deessejs/fp'
import { ForbiddenException } from '@deessejs/server'
export const requireRole = (role: 'admin' | 'moderator') =>
createMiddleware({
name: 'requireRole',
args: { role },
handler: async (ctx, opts) => {
if (ctx.user.role !== opts.args.role) {
return err(new ForbiddenException(`Requires ${opts.args.role} role`))
}
return opts.next()
},
})
// Usage
const adminOnly = t.mutation({
handler: async (ctx, args) => {
// Only admins reach here
return ok({ success: true })
},
}).use(requireRole('admin'))Context Modification
Middleware can modify the context for downstream handlers by passing a new context object to opts.next(). The example shows how to enrich context with computed properties:
import { createMiddleware } from '@deessejs/server'
const enrichContext = createMiddleware({
name: 'enrichContext',
handler: async (ctx, opts) => {
// Add computed properties to context
const enrichedCtx = {
...ctx,
ip: getClientIP(ctx),
userAgent: getUserAgent(ctx),
}
// Pass enriched context to next handler
return opts.next({ ctx: enrichedCtx })
},
})Multiple Middleware
When you use multiple middleware, they run in order from first to last. Each middleware can either pass control to the next or block the request:
import { defineContext } from '@deessejs/server'
import { corsMiddleware, authMiddleware, rateLimitMiddleware, requireRole } from './middleware'
const { t, createAPI } = defineContext({
context: () => ({ user: null, ip: '' }),
})
export const api = createAPI({
router: appRouter,
middleware: [
corsMiddleware, // 1. Handle CORS
authMiddleware, // 2. Check authentication
rateLimitMiddleware, // 3. Apply rate limits
requireRole('admin'), // 4. Check admin role
],
})Complete Example
The following example shows a full middleware setup with auth, requireAdmin, and rateLimit middleware chained together:
// src/middleware/index.ts
import { createMiddleware } from '@deessejs/server'
import { err } from '@deessejs/fp'
import { UnauthorizedException, ForbiddenException } from '@deessejs/server'
// Authentication middleware
export const auth = createMiddleware({
name: 'auth',
handler: async (ctx, opts) => {
if (!ctx.user) {
return err(new UnauthorizedException('Authentication required'))
}
return opts.next()
},
})
// Role check middleware
export const requireAdmin = createMiddleware({
name: 'requireAdmin',
handler: async (ctx, opts) => {
if (ctx.user.role !== 'admin') {
return err(new ForbiddenException('Admin access required'))
}
return opts.next()
},
})
// Rate limiting middleware (simplified)
export const rateLimit = createMiddleware({
name: 'rateLimit',
args: { maxRequests: 100 },
handler: async (ctx, opts) => {
const key = `ratelimit:${ctx.ip}`
const count = await redis.incr(key)
if (count > opts.args.maxRequests) {
return err(new Error('Rate limit exceeded'))
}
return opts.next()
},
})// src/api.ts
import { defineContext } from '@deessejs/server'
import { auth, requireAdmin, rateLimit } from './middleware'
const { t, createAPI } = defineContext({
context: () => ({ user: null, ip: '' }),
})
export const api = createAPI({
router: appRouter,
middleware: [
rateLimit({ maxRequests: 100 }),
auth,
],
})
// Admin-only procedure
const deleteAllUsers = t.mutation({
handler: async (ctx, args) => {
await ctx.db.users.deleteAll()
return ok({ success: true })
},
}).use(requireAdmin)Built-in Middleware Helpers
The library provides helper functions to wrap middleware for specific procedure types. You can use these to apply middleware more conveniently:
import { withQuery, withMutation } from '@deessejs/server/middleware/helpers'
// Apply to query type
const loggedQuery = withQuery(authMiddleware)
// Apply to mutation type
const auditedMutation = withMutation(auditMiddleware)Anti-Patterns
Don't Block in Middleware Without Error
When a middleware needs to block a request, you must return an error. Never just return without calling opts.next() or returning an error, as this causes requests to hang indefinitely:
import { createMiddleware } from '@deessejs/server'
import { err } from '@deessejs/fp'
import { ForbiddenException } from '@deessejs/server'
// Don't - silent block
const badMiddleware = createMiddleware({
name: 'bad',
handler: async (ctx, opts) => {
// Just returns without next() or error
return
},
})
// Do - return error or call next
const goodMiddleware = createMiddleware({
name: 'good',
handler: async (ctx, opts) => {
if (blocked) {
return err(new ForbiddenException('Blocked'))
}
return opts.next()
},
})Don't Modify Args in Middleware
Arguments should be treated as immutable. If you need to derive values from arguments, pass them through context instead to avoid confusing side effects:
import { createMiddleware } from '@deessejs/server'
// Don't - modifying args is confusing
const modifyArgs = createMiddleware({
name: 'modifyArgs',
handler: async (ctx, opts) => {
opts.args.id = 1 // Confusing side effects
return opts.next()
},
})
// Do - use context for modifications
const enrichContext = createMiddleware({
name: 'enrichContext',
handler: async (ctx, opts) => {
return opts.next({
ctx: { ...ctx, computed: computeFromArgs(opts.args) },
})
},
})Next Steps
- Lifecycle Hooks - Procedure-level hooks
- Context - Context best practices
- Security - Security model