Skip to content

Handlers

Handlers are functions that process requests. They receive an IAppEvent and return a value that becomes the response.

Core Handlers

A core handler processes a request and returns a response value:

typescript
import { defineCoreHandler } from 'routup';

const handler = defineCoreHandler((event) => {
    return { message: 'Hello, World!' };
});

Error Handlers

An error handler is called when an error occurs in a previous handler. It receives the error as the first argument and the event as the second:

typescript
import { defineErrorHandler } from 'routup';

const handler = defineErrorHandler((error, event) => {
    event.response.status = 500;
    return { error: error.message };
});

Return Values

Handlers return values that are automatically converted to Response objects:

typescript
// String — sent as text/plain
defineCoreHandler(() => 'Hello, World!');

// Object/Array — sent as application/json
defineCoreHandler(() => ({ name: 'Alice' }));

// Response — sent as-is
defineCoreHandler(() => new Response('Custom', { status: 201 }));

// ReadableStream, Blob, ArrayBuffer — sent as binary
defineCoreHandler(() => new Blob(['data']));

// null — empty response
defineCoreHandler(() => null);

Returning undefined

undefined is not an implicit pass-through. A handler that returns undefined is making a contract: it must have either called event.next() (forwarding the downstream result) or it must intend event.next() to be invoked later from an async callback.

  • Returning undefined after calling event.next() forwards the downstream result — event.next() and return event.next() are equivalent in outcome.
  • Returning undefined without ever calling event.next() leaves the handler unresolved. The pipeline waits until either event.next() is invoked or event.signal aborts. With a global or per-handler timeout, this surfaces as 408 Request Timeout. With no timeout configured, the request hangs by design — bugs become loud (deadlock) rather than silent (404 / wrong body).

Middleware

A handler that calls event.next() acts as middleware. It can inspect or modify the request, then pass control to the next handler:

typescript
defineCoreHandler(async (event) => {
    console.log(`${event.method} ${event.path}`);
    return event.next();
});

You can also modify the downstream response:

typescript
defineCoreHandler(async (event) => {
    const response = await event.next();
    // inspect or modify the response
    return response;
});

The result of event.next() is cached — calling it multiple times returns the same response.

Declaration Styles

Shorthand

Pass a function directly:

typescript
const handler = defineCoreHandler((event) => {
    return 'Hello, World!';
});

Verbose

Pass a configuration object with path, method, and handler function:

typescript
const handler = defineCoreHandler({
    method: 'GET',
    path: '/users/:id',
    fn: (event) => {
        return { id: event.params.id };
    }
});

The verbose form supports a per-handler timeout (in milliseconds) that overrides the router's handlerTimeout default:

typescript
const handler = defineCoreHandler({
    timeout: 5000, // 5 seconds for this handler
    fn: async (event) => {
        return fetchSlowData();
    }
});

Instrumentation callbacks

The verbose form also accepts onBefore / onAfter / onError — plain optional callbacks invoked around the handler's fn. They are intended for single-handler instrumentation (logging, metrics, per-handler auth checks); for instrumentation that should span multiple handlers, use middleware.

typescript
const handler = defineCoreHandler({
    fn: (event) => fetchUser(event.params.id!),
    onBefore: (event) => log.debug('user.fetch.start', { path: event.path }),
    onAfter:  (event, response) => log.debug('user.fetch.done', { status: response?.status }),
    onError:  (error, event) => metrics.increment('user.fetch.error', { code: error.status }),
});

Semantics:

  • onBefore fires before fn. Throwing here is treated like the handler throwing — onError (if set) fires next and the error propagates.
  • onAfter fires after the response is built (or the handler resolved without one). Receives (event, response). Throwing here is also treated like the handler throwing — onError fires next and the already-built response is dropped in favour of the error path.
  • onError fires when fn, onBefore, or onAfter throws. Receives (error, event). Re-throwing replaces event.error with the new error; returning normally lets the original propagate.

All three are skipped when an ErrorHandler is invoked with no pending event.error (no fn runs, so there is nothing to bracket).

Mounting

Global

typescript
app.use(defineCoreHandler((event) => {
    return event.next();
}));

By Method

typescript
app.get('/', defineCoreHandler((event) => 'Hello'));
app.post('/users', defineCoreHandler((event) => { /* ... */ }));

By Path

typescript
app.use('/api', defineCoreHandler((event) => {
    return { api: true };
}));

By Path and Method

typescript
app.get('/users/:id', defineCoreHandler((event) => {
    return { id: event.params.id };
}));