Skip to main content

stacks-middleware

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.

소스 정보

저장소
stacksjs/stacks
최근 소스 활동
2026년 9월 7일 04:05
감지된 SKILL.md 언어
영어
스타
631
포크
20

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

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
GitHub에서 보기