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:
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:
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:
// 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
undefinedafter callingevent.next()forwards the downstream result —event.next()andreturn event.next()are equivalent in outcome. - Returning
undefinedwithout ever callingevent.next()leaves the handler unresolved. The pipeline waits until eitherevent.next()is invoked orevent.signalaborts. With a global or per-handlertimeout, this surfaces as408 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:
defineCoreHandler(async (event) => {
console.log(`${event.method} ${event.path}`);
return event.next();
});You can also modify the downstream response:
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:
const handler = defineCoreHandler((event) => {
return 'Hello, World!';
});Verbose
Pass a configuration object with path, method, and handler function:
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:
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.
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:
onBeforefires beforefn. Throwing here is treated like the handler throwing —onError(if set) fires next and the error propagates.onAfterfires after the response is built (or the handler resolved without one). Receives(event, response). Throwing here is also treated like the handler throwing —onErrorfires next and the already-built response is dropped in favour of the error path.onErrorfires whenfn,onBefore, oronAfterthrows. Receives(error, event). Re-throwing replacesevent.errorwith 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
app.use(defineCoreHandler((event) => {
return event.next();
}));By Method
app.get('/', defineCoreHandler((event) => 'Hello'));
app.post('/users', defineCoreHandler((event) => { /* ... */ }));By Path
app.use('/api', defineCoreHandler((event) => {
return { api: true };
}));By Path and Method
app.get('/users/:id', defineCoreHandler((event) => {
return { id: event.params.id };
}));