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')
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