Skip to content
Back to skills

Stacks Middleware

ASecurity

Use when working with middleware in a Stacks application - defining middleware, applying to routes, middleware aliases, parameterized middleware, groups, or the middleware execution pipeline. Covers the Middleware class, app/Middleware.ts alias registry, and all 22 default middleware files.

  • 631 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 5, 2026
developmenttypescriptgobashapifrontendperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 20, 2026

npx -y skills add stacksjs/stacks --skill stacks-middleware --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Stacks Middleware?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Stacks Middleware
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/stacksjs-stacks-middleware/badge)](https://www.skillsdirectory.com/skills/stacksjs-stacks-middleware)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: stacks-middleware
description: Use when working with middleware in a Stacks application - defining middleware, applying to routes, middleware aliases, parameterized middleware, groups, or the middleware execution pipeline. Covers the Middleware class, app/Middleware.ts alias registry, and all 22 default middleware files.
license: MIT
compatibility: Bun >= 1.3.0, TypeScript
allowed-tools: Read Edit Write Bash Grep Glob
---

# Stacks Middleware

Built into `@stacksjs/router`. Middleware intercepts requests before they reach route handlers.

## Key Paths
- Middleware class: `storage/framework/core/router/src/middleware.ts`
- Execution engine: `storage/framework/core/router/src/stacks-router.ts`
- Alias registry: `app/Middleware.ts`
- Default middleware: `storage/framework/defaults/app/Middleware/` (22 files)
- Auth middleware (standalone): `storage/framework/core/auth/src/middleware.ts`
- Tests: `storage/framework/core/router/tests/middleware.test.ts`

## Middleware Class

```typescript
import { Middleware } from '@stacksjs/router'

export interface MiddlewareConfig {
  name: string
  priority?: number  // Lower = runs first, default: 10
  handle: (request: EnhancedRequest) => void | Promise<void>
}

export class Middleware {
  readonly name: string
  readonly priority: number
  readonly handle: (request: EnhancedRequest) => void | Promise<void>

  constructor(config: MiddlewareConfig)
}
```

## Creating Custom Middleware

Create a file in `app/Middleware/`:

```typescript
// app/Middleware/RateLimit.ts
import { Middleware } from '@stacksjs/router'

export default new Middleware({
  name: 'RateLimit',
  priority: 2,
  async handle(request) {
    // Throw to short-circuit the request
    // throw new HttpError(429, 'Too many requests')

    // Return void to continue to next middleware
  },
})
```

### Short-Circuiting

Middleware can stop request processing by:
1. **Throwing an Error with `statusCode`** — converted to HTTP error response
2. **Throwing a Response** — returned directly to client
3. **Returning void** — continues to next middleware

```typescript
// Error with status code
const error = new Error('Unauthorized') as Error & { statusCode: number }
error.statusCode = 401
throw error

// Direct Response (full control)
throw new Response(JSON.stringify({ error: 'Rate limited' }), {
  status: 429,
  headers: { 'Retry-After': '60' },
})
```

## Alias Registry (app/Middleware.ts)

Maps short names to middleware class filenames:

```typescript
import { defineMiddleware } from '@stacksjs/router'

export default defineMiddleware({
  'maintenance': 'Maintenance',
  'auth': 'Auth',
  'guest': 'Guest',
  'api': 'Api',
  'team': 'Team',
  'site': 'Site',
  'logger': 'Logger',
  'abilities': 'Abilities',
  'can': 'Can',
  'throttle': 'Throttle',
  'signed': 'Signed',
  'env': 'Env',
  'env:local': 'EnvLocal',
  'env:development': 'EnvDevelopment',
  'env:dev': 'EnvDevelopment',
  'env:staging': 'EnvStaging',
  'env:production': 'EnvProduction',
  'env:prod': 'EnvProduction',
  'role': 'Role',
  'permission': 'Permission',
  'verified': 'EnsureEmailIsVerified',
})
```

The alias is yours to invent; the class name is checked against
`app/Middleware/` and the framework defaults, so `{ auth: 'Auht' }` is a
compile error rather than a route whose guard resolves to nothing.

This map is **merged over** the framework defaults, not a replacement for them,
so an alias Stacks adds later is available without editing the file.

## Reference forms

Three shapes are read off a reference, in this order:

| Written | Means |
|---|---|
| `'auth'` | the alias, or a class name if no alias matches (`'signed'` → `Signed`) |
| `'!auth'` | inverted: the route passes only when `auth` refuses |
| `'throttle:60,1'` | `throttle` with `60,1` in `request._middlewareParams.throttle` |

The **whole** reference is looked up as an alias before the colon is treated as
a parameter separator. That is what makes `'env:production'` its own alias
rather than `env` with a parameter - `Env` ignores parameters and accepts every
known environment, so splitting first turned a production-only route into an
unguarded one.

Inversion counts a `Response` or a status-carrying error as a refusal, and
nothing else. A `TypeError` from a bug inside `Auth` is a crash, not a
declination, and must not let `!auth` through.

## Applying Middleware

### Per-Route (Chainable)
```typescript
route.get('/dashboard', 'DashboardAction')
  .middleware('auth')
  .middleware('verified')
  .name('dashboard.show')
```

### Route Groups
```typescript
route.group({ prefix: '/api/v1', middleware: ['auth', 'throttle'] }, () => {
  route.get('/users', listUsers)
  route.post('/users', createUser)
})
```

### STX Pages

The frontend and API share the same middleware definitions. Stacks loads the
aliases from `app/Middleware.ts` and the class-style handlers from
`app/Middleware/` into the normal stx dev and production page servers.

Declare an alias from that registry in a page's server metadata:

```stx
<script server>
definePageMeta({ middleware: ['auth', 'verified', 'role:admin'] })
</script>
```

Do not create a second frontend-only middleware registry. A custom middleware
is defined once with `new Middleware({ name, priority, handle })`, added to
`app/Middleware.ts`, and may then guard an API route, an stx page, or both.

STX prepares the incoming request with the same EnhancedRequest helpers before
calling `handle`, sorts the combined page chain by priority, writes parameters
to `request._middlewareParams`, supports exact aliases containing colons and
`!alias` inversion, and fails closed when an alias is missing.

Group middleware is prepended to all routes inside the callback. Groups can be nested — middleware accumulates.

### Parameterized Middleware

Pass parameters using colon syntax:

```typescript
route.get('/admin', handler).middleware('throttle:60,1')     // 60 requests per 1 minute
route.get('/posts', handler).middleware('abilities:read,write')
route.get('/settings', handler).middleware('role:admin,editor')
route.get('/posts/{id}', handler).middleware('can:update,post')
```

Parameters are stored on `request._middlewareParams[middlewareName]` and parsed by the middleware's `handle` function.

## Default Middleware Reference

| Alias | Class | Priority | Description |
|-------|-------|----------|-------------|
| `maintenance` | Maintenance | 0 | Checks maintenance mode, supports secret bypass URL and IP allowlist |
| `auth` | Auth | 1 | Validates bearer token, sets authenticated user on request |
| `api` | Api | 1 | Validates request accepts JSON |
| `guest` | Guest | 1 | Ensures user is NOT authenticated (for login/register pages) |
| `env` | Env | 1 | Checks current environment |
| `logger` | Logger | 2 | Logs request method and URL |
| `abilities` | Abilities | 2 | Checks token abilities/scopes (parameterized: `abilities:read,write`) |
| `can` | Can | 3 | Authorization gate (parameterized: `can:update,post`) |
| `role` | Role | 3 | Checks user roles (parameterized: `role:admin`) |
| `permission` | Permission | 3 | Checks user permissions (parameterized: `permission:edit-posts`) |
| `team` | Team | 3 | Ensures user belongs to a team (parameterized: `team:teamId`) |
| `verified` | EnsureEmailIsVerified | 4 | Verifies email is confirmed |
| `throttle` | Throttle | — | Rate limiting (parameterized: `throttle:60,1` or `throttle:100,5m`) |
| `env:local` | EnvLocal | — | Only allows local environment |
| `env:development` / `env:dev` | EnvDevelopment | — | Only allows development |
| `env:staging` | EnvStaging | — | Only allows staging |
| `env:production` / `env:prod` | EnvProduction | — | Only allows production |

### Environment Negation Variants

`EnvNotLocal`, `EnvNotDevelopment`, `EnvNotStaging` and `EnvNotProduction` exist
as classes with no alias. Reference them by class name, or write the negated
form of the positive alias - `'!env:production'` is `EnvNotProduction`.

## Middleware Loading Flow

```
1. Strip a leading '!', if any, and remember it
2. Look the WHOLE remainder up in the merged alias map
   'env:production' is an alias      → { name: 'env:production' }
   'throttle:60,1' is not            → { name: 'throttle', params: '60,1' }
3. Check middleware cache (loaded once, cached for performance)
4. Resolve alias: 'throttle' → 'Throttle'; unaliased names PascalCase
5. Try loading from app/Middleware/Throttle.ts (user overrides)
6. Fall back to storage/framework/defaults/app/Middleware/Throttle.ts
7. Store params on request: request._middlewareParams.throttle = '60,1'
8. Wrap in the inverter if step 1 saw a '!'
9. Sort the chain by priority, then execute: await middleware.handle(req)
```

User middleware in `app/Middleware/` always takes precedence over framework defaults.

## Representative Implementations

### Auth Middleware
```typescript
export default new Middleware({
  name: 'Auth',
  priority: 1,
  async handle(request) {
    const bearerToken = request.bearerToken()
    if (!bearerToken)
      throw new HttpError(401, 'Unauthorized. No token provided.')
    const isValid = await Auth.validateToken(bearerToken)
    if (!isValid)
      throw new HttpError(401, 'Unauthorized. Invalid token.')
  },
})
```

### Maintenance Middleware
```typescript
export default new Middleware({
  name: 'Maintenance',
  priority: 0,
  async handle(request) {
    if (!(await isDownForMaintenance())) return
    const payload = await maintenancePayload()
    // Check secret bypass URL → set cookie and redirect
    // Check bypass cookie or allowed IP → continue
    // Otherwise → throw maintenance response
  },
})
```

### Throttle Middleware (Parameterized)
```typescript
// '60,1' = 60 requests per 1 minute
// '100,5m' = 100 requests per 5 minutes
// '1000,1h' = 1000 requests per hour
async handle(request) {
  const params = request._middlewareParams?.throttle || '60,1'
  const config = parseThrottleString(params)
  const limiter = createRateLimitMiddleware(config)
  // Returns 429 if rate limit exceeded
}
```

## Standalone Auth Middleware (@stacksjs/auth)

A separate, more complete auth middleware exists:

```typescript
import { authMiddleware, authMiddlewareHandler } from '@stacksjs/auth'

// Checks both request.bearerToken() and raw Authorization header
// Calls Auth.getUserFromToken() to validate
// Sets Auth.setUser(user) and request._authenticatedUser
// Loads current access token onto request._currentAccessToken
```

## Gotchas
- **Priority DOES order the chain** — entries are sorted by `priority` (lower first, default 10) before execution, so CORS can precede auth regardless of the order they were attached in. An earlier version of this file said otherwise. A non-finite or negative value is clamped to the default and warned about once
- **`terminate()` doesn't exist** — some docs reference it, but it's not in the actual `MiddlewareConfig` interface
- **Two auth middleware implementations** — defaults version (basic token check) and `@stacksjs/auth` version (full user loading)
- **EnvNot* files have no aliases** — reachable by class name, or as `'!env:production'` and friends
- **The alias map merges over the defaults** — an app's `app/Middleware.ts` adds to and overrides them rather than replacing the set
- **Middleware is cached after first load** — changes require server restart
- **User overrides take precedence** — `app/Middleware/Auth.ts` replaces the framework default completely
- **Group middleware accumulates** — nested groups combine all parent middleware
- **Request body parsed before middleware** — Laravel-style methods (`.input()`, `.query`, `.file()`) are available in middleware

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.