On this page 27
Middleware Parameters
Passing Parameters
Pass parameters to middleware using the colon syntax:
// Throttle: 60 requests per minute
route.post('/api/data', 'Actions/DataAction')
.middleware('throttle:60,1')
// Abilities: require 'read' and 'write' abilities
route.post('/posts', 'Actions/PostStoreAction')
.middleware('abilities:read,write')
// Environment: only run in production
route.get('/metrics', 'Actions/MetricsAction')
.middleware('env:production')
Accessing Parameters in Middleware
// app/Middleware/Abilities.ts
import type { Request } from '@stacksjs/router'
import { HttpError } from '@stacksjs/error-handling'
import { Middleware } from '@stacksjs/router'
export default new Middleware({
name: 'abilities',
priority: 2,
async handle(request: Request) {
// Parameters are available via _middlewareParams
const requiredAbilities = (request as any)._middlewareParams?.abilities?.split(',') || []
if (requiredAbilities.length === 0) {
return // No abilities required
}
const token = (request as any)._currentAccessToken
if (!token) {
throw new HttpError(401, 'Unauthenticated.')
}
const tokenAbilities: string[] = token.abilities || []
// Wildcard grants all permissions
if (tokenAbilities.includes('_')) {
return
}
// Check each required ability
for (const ability of requiredAbilities) {
if (!tokenAbilities.includes(ability.trim())) {
throw new HttpError(403, `Missing required ability: ${ability.trim()}`)
}
}
},
})
Terminable Middleware
Terminable middleware can perform actions after the response has been sent:
// app/Middleware/LogResponse.ts
import { Middleware } from '@stacksjs/router'
import { log } from '@stacksjs/logging'
export default new Middleware({
name: 'LogResponse',
priority: 100, // Run late
handle(request) {
const startTime = Date.now()
;(request as any)._startTime = startTime
},
// Called after response is sent
terminate(request, response) {
const startTime = (request as any)._startTime || Date.now()
const duration = Date.now() - startTime
log.info(`[${request.method}] ${request.url} - ${response.status} (${duration}ms)`)
},
})
Built-in Middleware
Auth Middleware
Validates bearer tokens and authenticates users:
route.get('/me', 'Actions/AuthUserAction').middleware('auth')
The auth middleware:
- Extracts the Bearer token from Authorization header
- Validates the token
- Attaches the user to
request._authenticatedUser - Attaches the token to
request._currentAccessToken
Abilities Middleware
Checks if the authenticated user's token has required abilities:
// Require 'admin' ability
route.get('/admin', 'Actions/AdminAction')
.middleware('auth')
.middleware('abilities:admin')
// Require multiple abilities
route.post('/posts', 'Actions/PostAction')
.middleware('auth')
.middleware('abilities:posts:create,posts:update')
Throttle Middleware
Rate limits requests:
// 60 requests per minute
route.post('/api/submit', 'Actions/SubmitAction')
.middleware('throttle:60,1')
// 10 requests per hour
route.post('/expensive-operation', 'Actions/ExpensiveAction')
.middleware('throttle:10,60')
Every limit is multiplied by config.security.rateLimit.scale (or
STACKS_RATE_LIMIT_SCALE), which stays 1 in production; a test suite can
raise it instead of turning limits off. The Throttle middleware counts in
process memory. Route .rateLimit(max, window) and the rateLimit() helper
count in the Redis cache when config/cache.ts selects it, so their budgets
hold across every worker and instance, and fall back to memory while Redis is
unreachable.
An anonymous request spends its client address's budget, as clientAddress()
from @stacksjs/router resolves it: the socket peer, or the forwarding headers
of a trusted proxy. config.security.trustedProxies lists those proxies
(addresses or CIDR ranges). Unset, it trusts loopback and private networks; set
it to your load balancer's ranges when other hosts share that network, or to
[] when nothing sits in front of the app.
Environment Middleware
Restricts routes to specific environments:
// Only in local environment
route.get('/debug', 'Actions/DebugAction')
.middleware('env:local')
// Only in production
route.get('/metrics', 'Actions/MetricsAction')
.middleware('env:production')
// Only in development
route.get('/test', 'Actions/TestAction')
.middleware('env:development')
Maintenance Middleware
Blocks requests when application is in maintenance mode:
route.group({ middleware: 'maintenance' }, () => {
// These routes will return 503 during maintenance
route.get('/api/data', 'Actions/DataAction')
})
Middleware Priority
Setting Priority
Lower priority numbers run first:
export default new Middleware({
name: 'Auth',
priority: 1, // Runs before priority 2
handle(request) {
// Authentication logic
},
})
export default new Middleware({
name: 'Abilities',
priority: 2, // Runs after priority 1
handle(request) {
// Ability checking (needs authenticated user)
},
})
Execution Order
- Global middleware (in order of registration)
- Route group middleware (in order of definition)
- Route-specific middleware (in order of chaining)
Creating Custom Middleware
Logger Middleware
// app/Middleware/Logger.ts
import { Middleware } from '@stacksjs/router'
import { log } from '@stacksjs/logging'
export default new Middleware({
name: 'Logger',
priority: 0, // Run first
handle(request) {
const timestamp = new Date().toISOString()
log.info(`[${timestamp}] ${request.method} ${request.url}`)
},
})
CORS Middleware
// app/Middleware/Cors.ts
import { Middleware } from '@stacksjs/router'
export default new Middleware({
name: 'Cors',
priority: 0,
handle(request) {
// CORS is typically handled at the response level
// This middleware can set up request-level CORS checks
const origin = request.headers.get('Origin')
if (origin && !isAllowedOrigin(origin)) {
throw new HttpError(403, 'Origin not allowed')
}
},
})
function isAllowedOrigin(origin: string): boolean {
const allowedOrigins = ['https://example.com', 'https://app.example.com']
return allowedOrigins.includes(origin)
}
Request Sanitization Middleware
// app/Middleware/Sanitize.ts
import { Middleware } from '@stacksjs/router'
export default new Middleware({
name: 'Sanitize',
priority: 5,
handle(request) {
// Sanitize all string inputs
if ((request as any).jsonBody) {
sanitizeObject((request as any).jsonBody)
}
if ((request as any).formBody) {
sanitizeObject((request as any).formBody)
}
},
})
function sanitizeObject(obj: Record<string, any>): void {
for (const key in obj) {
if (typeof obj[key] === 'string') {
obj[key] = obj[key].trim()
} else if (typeof obj[key] = 'object' && obj[key] ! null) {
sanitizeObject(obj[key])
}
}
}
Edge Cases and Gotchas
Middleware Order with Groups
When using nested groups, middleware stacks:
route.group({ middleware: 'auth' }, () => {
// 'auth' middleware applies here
route.group({ middleware: 'logger' }, () => {
// Both 'auth' AND 'logger' apply here
route.get('/data', 'Actions/DataAction')
})
})
Async Middleware Errors
Always throw HttpError for proper error responses:
import { HttpError } from '@stacksjs/error-handling'
export default new Middleware({
name: 'CustomAuth',
async handle(request) {
try {
await validateRequest(request)
} catch (error) {
// Throw HttpError for proper status codes
throw new HttpError(401, 'Authentication failed')
}
},
})
Middleware Short-Circuiting
When middleware throws an error, the request chain stops:
export default new Middleware({
name: 'BlockedRoute',
handle() {
// This stops all further processing
throw new HttpError(403, 'This route is blocked')
},
})
Accessing Request Data
After body parsing, data is available via:
handle(request) {
// JSON body
const jsonData = (request as any).jsonBody
// Form data
const formData = (request as any).formBody
// Files
const files = (request as any).files
// Route parameters
const params = request.params
// Query parameters
const query = (request as any).query
}
API Reference
Middleware Class
class Middleware {
constructor(options: {
name: string
priority?: number
handle: (request: Request) => void | Promise<void>
terminate?: (request: Request, response: Response) => void | Promise<void>
})
}
HttpError Class
class HttpError extends Error {
constructor(statusCode: number, message: string)
statusCode: number
message: string
}
Request Extensions
After middleware processing, the request object includes:
| Property | Type | Description |
|---|---|---|
_authenticatedUser | User | Authenticated user (after auth middleware) |
_currentAccessToken | Token | Current access token (after auth middleware) |
_middlewareParams | Record<string, string> | Middleware parameters |
jsonBody | object | Parsed JSON body |
formBody | object | Parsed form data |
files | Record<string, File> | Uploaded files |
Related Documentation
- Routing - Learn about route definitions
- Actions - Creating action handlers
- Error Handling - Handling middleware errors
- Authentication - Authentication system