Middleware Composition

Middleware Composition

VIEW

The compose() helper allows you to combine multiple middlewares into a single reusable middleware handler. This is useful for creating middleware stacks that can be applied consistently across multiple routes or controllers.

Why Use Compose?

  • Reusability: Define a middleware stack once, use it everywhere
  • Cleaner Code: Avoid repeating multiple @UseMiddleware decorators
  • Flexibility: Mix Hono functions, Lockness classes, and named middlewares
  • Nesting: Build complex stacks from simpler ones

Import

typescript
import { compose, ComposeMiddleware, composeMiddleware } from '@lockness/core'

@ComposeMiddleware Decorator (Recommended)

The @ComposeMiddleware decorator provides the cleanest syntax for inline middleware composition. It combines compose() and @UseMiddleware() in a single decorator:

typescript
import { ComposeMiddleware, cors, logger } from '@lockness/core'

@Controller('/api')
export class ApiController {
    @Get('/users')
    @ComposeMiddleware(logger(), AuthMiddleware, 'admin')
    users(c: Context) {
        return c.json({ users: [] })
    }
}

Benefits:

  • ✅ No intermediate variable needed
  • ✅ Single import (ComposeMiddleware)
  • ✅ Inline declaration at the route level
  • ✅ Supports all middleware types (functions, classes, named)

Comparison

ApproachLinesImports Required
@ComposeMiddleware(...)1ComposeMiddleware
compose() + @UseMiddleware()2-4compose, UseMiddleware

Basic Usage (Alternative)

If you prefer to define reusable stacks or need more control, use compose() with @UseMiddleware():

Array Syntax

typescript
import { compose, cors, logger } from '@lockness/core'

const apiStack = compose([
    logger(), // Hono function middleware
    AuthMiddleware, // Lockness class middleware
    'admin', // Named middleware (resolved from registry)
])

@Controller('/api')
export class ApiController {
    @Get('/users')
    @UseMiddleware(apiStack)
    users(c: Context) {
        return c.json({ users: [] })
    }
}

Rest Parameters Syntax

typescript
import { composeMiddleware, cors, logger } from '@lockness/core'

const stack = composeMiddleware(
    logger(),
    AuthMiddleware,
    'admin',
)

Supported Middleware Types

The compose() function accepts any combination of:

TypeExampleDescription
Hono functionscors(), logger()Built-in Hono middleware
Class middlewaresAuthMiddlewareClasses implementing IMiddleware
Named middlewares'auth', 'admin'Registered via @DeclareMiddleware
Composed middlewaresauthStackOutput of another compose() call

Nested Composition

You can compose composed middlewares for complex stacks:

typescript
// Layer 1: Base authentication
const authStack = compose([
    sessionMiddleware(),
    'auth',
])

// Layer 2: Admin-specific (includes auth)
const adminStack = compose([
    authStack,          // Reuse the auth stack
    'admin',
    AuditMiddleware,
])

// Layer 3: Super admin (includes admin)
const superAdminStack = compose([
    adminStack,
    SuperAdminMiddleware,
    'rate-limit',
])

// Use the complete stack
@Controller('/super-admin')
@UseMiddleware(superAdminStack)
export class SuperAdminController { ... }

Execution Order

Middlewares execute in the order they appear in the array, following Hono's "onion" model:

typescript
const stack = compose([m1, m2, m3])

// Execution order:
// m1 → m2 → m3 → handler → m3 → m2 → m1

Example with logging:

typescript
const loggingStack = compose([
    async (c, next) => {
        console.log('1. First middleware START')
        await next()
        console.log('6. First middleware END')
    },
    async (c, next) => {
        console.log('2. Second middleware START')
        await next()
        console.log('5. Second middleware END')
    },
    async (c, next) => {
        console.log('3. Third middleware START')
        await next()
        console.log('4. Third middleware END')
    },
])

// Output:
// 1. First middleware START
// 2. Second middleware START
// 3. Third middleware START
// 4. Third middleware END
// 5. Second middleware END
// 6. First middleware END

Short-Circuiting

If any middleware returns a Response without calling next(), the chain stops immediately:

typescript
const protectedStack = compose([
    async (c, next) => {
        const user = c.get('user')
        if (!user) {
            // Short-circuit: return early, don't call next()
            return c.json({ error: 'Unauthorized' }, 401)
        }
        await next()
    },
    AdminMiddleware, // Only runs if user exists
    AuditMiddleware, // Only runs if user exists
])

Common Patterns

API Authentication Stack

typescript
const apiAuthStack = compose([
    cors({ origin: ['https://app.example.com'] }),
    bearerAuth({ token: Deno.env.get('API_TOKEN')! }),
    requestId(),
    logger(),
])

@Controller('/api/v1')
@UseMiddleware(apiAuthStack)
export class ApiV1Controller { ... }

Rate-Limited Public Endpoints

typescript
const publicStack = compose([
    cors(),
    compress(),
    RateLimitMiddleware,
])

@Controller('/public')
@UseMiddleware(publicStack)
export class PublicController { ... }

Development vs Production

typescript
const baseStack = compose([
    sessionMiddleware(),
    'auth',
])

const devStack = compose([
    baseStack,
    logger(), // Extra logging in dev
])

const prodStack = compose([
    baseStack,
    compress(),
    secureHeaders(),
])

const appStack = Deno.env.get('APP_ENV') === 'production' ? prodStack : devStack

API Reference

compose(middlewares)

Composes an array of middlewares into a single middleware handler.

Parameters:

  • middlewares - Array of ComposableMiddleware (functions, classes, or strings)

Returns: MiddlewareHandler

typescript
const stack = compose([
    cors(),
    AuthMiddleware,
    'admin',
])

composeMiddleware(...middlewares)

Alternative syntax using rest parameters instead of an array.

Parameters:

  • ...middlewares - Rest parameters of ComposableMiddleware

Returns: MiddlewareHandler

typescript
const stack = composeMiddleware(
    cors(),
    AuthMiddleware,
    'admin',
)

ComposableMiddleware Type

typescript
type ComposableMiddleware =
    | MiddlewareHandler                              // Hono function
    | { new(): { handle: (c, next) => ... } }        // Lockness class
    | string                                          // Named middleware

Error Handling

If a named middleware is not found in the registry, it is skipped with a warning:

typescript
const stack = compose([
    'auth',
    'non-existent-middleware', // ⚠️ Warning logged, skipped
    'admin',
])

To avoid this, ensure all named middlewares are registered via @DeclareMiddleware before the composed stack is used.

Best Practices

  1. Name your stacks descriptively: authStack, apiStack, adminStack
  2. Keep stacks focused: One stack for auth, another for API config
  3. Document complex stacks: Add comments explaining the middleware order
  4. Test composed stacks: Write integration tests for critical stacks
  5. Consider environment: Create separate stacks for dev/prod if needed