Skip to content

Interceptors ​

Interceptors hook into the request lifecycle to run logic before and after your handlers. They're the foundation for authentication, logging, error handling, and response transformation.

Lifecycle ​

Every request passes through interceptors in this order:

  1. Before hooks — run before the handler (can reply early)
  2. Handler execution — your route handler runs
  3. After hooks — run after the handler (can transform the response)
  4. Error hooks — run if an error is thrown at any point

TIP

See the Event Lifecycle Diagram for the full picture of how interceptors fit into the request flow.

Writing functional interceptors ​

Before interceptor ​

Use defineBeforeInterceptor to run logic before the handler. Call reply(value) to skip the handler and respond immediately:

ts
import { defineBeforeInterceptor, TInterceptorPriority } from 'moost'
import { useRequest } from '@wooksjs/event-http'

const cacheInterceptor = defineBeforeInterceptor((reply) => {
    const cached = cache.get(useRequest().url)
    if (cached) {
        reply(cached) // skip handler, respond with cached value
    }
}, TInterceptorPriority.INTERCEPTOR)

After interceptor ​

Use defineAfterInterceptor to run logic after the handler succeeds. Receives the handler's response; call reply(newValue) to replace it:

ts
import { defineAfterInterceptor, TInterceptorPriority } from 'moost'

const wrapper = defineAfterInterceptor((response, reply) => {
    reply({ data: response, timestamp: Date.now() })
}, TInterceptorPriority.AFTER_ALL)

Error interceptor ​

Use defineErrorInterceptor to handle errors. Receives the thrown error; call reply(value) to send a custom error response:

ts
import { defineErrorInterceptor, TInterceptorPriority } from 'moost'
import { HttpError } from '@moostjs/event-http'

const errorFormatter = defineErrorInterceptor((error, reply) => {
    reply({
        error: error.message,
        code: error instanceof HttpError ? error.body.statusCode : 500,
    })
}, TInterceptorPriority.CATCH_ERROR)

Full interceptor ​

Combine multiple hooks in a single definition with defineInterceptor:

ts
import { defineInterceptor, TInterceptorPriority } from 'moost'
import { useRequest } from '@wooksjs/event-http'
import { useLogger } from '@wooksjs/event-core'

const requestLogger = defineInterceptor({
    after() {
        const { url, method } = useRequest()
        useLogger('http').info(`${method} ${url} OK`)
    },
    error(error) {
        const { url, method } = useRequest()
        useLogger('http').error(`${method} ${url} FAILED: ${error.message}`)
    },
}, TInterceptorPriority.BEFORE_ALL)

Priority levels ​

Interceptors run in priority order, lowest first:

PriorityValueUse case
BEFORE_ALL0Setup, timing, context
BEFORE_GUARD1Pre-auth logic
GUARD2Authentication & authorization
AFTER_GUARD3Post-auth setup
INTERCEPTOR4General purpose (default)
CATCH_ERROR5Error formatting
AFTER_ALL6Cleanup, final headers

Each helper uses a sensible default:

HelperDefault priority
defineBeforeInterceptorINTERCEPTOR
defineAfterInterceptorAFTER_ALL
defineErrorInterceptorCATCH_ERROR
defineInterceptorINTERCEPTOR

Unwind order

Only the before hooks run in ascending priority order. After/error hooks unwind in reverse (onion model): the after hook of a higher-priority interceptor runs closer to the handler, so a BEFORE_ALL interceptor's after hook fires last — wrapping everything else.

Applying interceptors ​

Per handler ​

ts
import { Intercept } from 'moost'

@Controller()
export class ExampleController {
    @Get('secret')
    @Intercept(guard)
    secret() { /* protected */ }

    @Get('public')
    publicRoute() { /* not protected */ }
}

Per controller ​

All handlers in the controller are intercepted:

ts
@Intercept(guard)
@Controller('admin')
export class AdminController {
    @Get('dashboard')
    dashboard() { /* protected */ }

    @Get('settings')
    settings() { /* also protected */ }
}

Globally ​

Affects every handler in the application:

ts
const app = new Moost()
app.applyGlobalInterceptors(timing, errorFormatter)

Turning interceptors into decorators ​

For cleaner syntax, wrap an interceptor with Intercept to create a reusable decorator:

ts
import { Intercept, defineBeforeInterceptor, TInterceptorPriority } from 'moost'
import { HttpError } from '@moostjs/event-http'

const guardFn = defineBeforeInterceptor((reply) => {
    if (!isAuthorized()) {
        throw new HttpError(401)
    }
}, TInterceptorPriority.GUARD)

// Create a decorator
const RequireAuth = Intercept(guardFn)

// Use it
@RequireAuth
@Controller('admin')
export class AdminController { /* ... */ }

Parameterized decorators ​

Create decorator factories for configurable interceptors:

ts
const RequireRole = (role: string) => {
    const fn = defineBeforeInterceptor((reply) => {
        if (!hasRole(role)) {
            throw new HttpError(403, `Requires role: ${role}`)
        }
    }, TInterceptorPriority.GUARD)
    return Intercept(fn)
}

@RequireRole('admin')
@Controller('admin')
export class AdminController { /* ... */ }

Class-based interceptors ​

When you need dependency injection in your interceptor, use a class with the @Interceptor decorator:

ts
import {
    Interceptor, Before, After, OnError,
    Overtake, Response, TInterceptorPriority,
} from 'moost'
import type { TOvertakeFn } from 'moost'
import { HttpError } from '@moostjs/event-http'

@Interceptor(TInterceptorPriority.GUARD)
export class AuthInterceptor {
    constructor(private authService: AuthService) {}

    @Before()
    check() {
        if (!this.authService.isAuthenticated()) {
            throw new HttpError(401)
        }
    }
}

Use @Overtake() and @Response() parameter decorators to access the reply function and handler result:

ts
@Interceptor(TInterceptorPriority.CATCH_ERROR)
export class ErrorHandler {
    @OnError()
    handle(@Response() error: Error, @Overtake() reply: TOvertakeFn) {
        reply({ message: error.message })
    }
}

Apply class-based interceptors the same way:

ts
@Intercept(AuthInterceptor)
@Controller()
export class ProtectedController { /* ... */ }

// or globally
app.applyGlobalInterceptors(AuthInterceptor)

Moost creates the interceptor class through DI, so constructor dependencies are injected automatically.

Practical example: request logger ​

ts
import { Interceptor, After, OnError, TInterceptorPriority } from 'moost'
import { useRequest } from '@wooksjs/event-http'
import { useLogger } from '@wooksjs/event-core'

@Interceptor(TInterceptorPriority.BEFORE_ALL)
export class RequestLogger {
    @After()
    logSuccess() {
        const { url, method } = useRequest()
        useLogger('http').info(`${method} ${url} OK`)
    }

    @OnError()
    logError(@Response() error: Error) {
        const { url, method } = useRequest()
        useLogger('http').error(`${method} ${url} FAILED: ${error.message}`)
    }
}

Practical example: error handler ​

ts
import { defineErrorInterceptor, TInterceptorPriority } from 'moost'
import { HttpError } from '@moostjs/event-http'
import { useResponse } from '@wooksjs/event-http'

export const errorHandler = defineErrorInterceptor((error, reply) => {
    const response = useResponse()
    const status = error instanceof HttpError ? error.body.statusCode : 500
    response.status = status
    reply({
        error: error.message,
        statusCode: status,
    })
}, TInterceptorPriority.CATCH_ERROR)

Released under the MIT License.