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

  1. Global middleware (in order of registration)
  2. Route group middleware (in order of definition)
  3. 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:

PropertyTypeDescription
_authenticatedUserUserAuthenticated user (after auth middleware)
_currentAccessTokenTokenCurrent access token (after auth middleware)
_middlewareParamsRecord<string, string>Middleware parameters
jsonBodyobjectParsed JSON body
formBodyobjectParsed form data
filesRecord<string, File>Uploaded files