メインコンテンツまでスキップ
バージョン: 🚧 Canary

🔧 Handler

A handler is an async function that contains the core business logic of your API endpoint. It receives a payload containing request data and context, and returns same payload object that contains the response data. For WebSocket routes, handlers return RxJS Subjects for real-time communication.


📋 Handler Types

Nodeblocks provides two main handler types based on their role and protocol:

HTTP Handlers

Handlers that perform business logic operations (database calls, data validation, transformations, etc.) and return Result<RouteHandlerPayload, NodeblocksError>:

import { ok, err, Result } from 'neverthrow';
import { primitives, handlers, utils } from '@nodeblocks/backend-sdk';

const { NodeblocksError, AsyncRouteHandler, RouteHandlerPayload } = primitives;
const { mergeData } = handlers;
const { createBaseEntity } = utils;

// Async handler for database operations
export const createUser: AsyncRouteHandler<
Result<RouteHandlerPayload, NodeblocksError>
> = async (payload) => {
const { params, context } = payload;

if (!params.requestBody || Object.keys(params.requestBody).length === 0) {
return err(
new NodeblocksError(400, 'Request body is required', 'createUser')
);
}

const baseEntity = createBaseEntity(params.requestBody);
try {
const createdUser = await context.db.users.insertOne(baseEntity);

if (!createdUser.insertedId) {
return err(
new NodeblocksError(400, 'Failed to create user', 'createUser')
);
}

return ok(mergeData(payload, { userId: baseEntity.id }));
} catch (_error) {
return err(new NodeblocksError(500, 'Failed to create user', 'createUser'));
}
};

// Sync handler for data transformation
export const formatUserData: RouteHandler<
Result<RouteHandlerPayload, NodeblocksError>
> = (payload) => {
const { requestBody } = payload.params;

const user = {
name: `${requestBody.firstName} ${requestBody.lastName}`
};

return ok(mergeData(payload, { user }));
};

Terminator Handlers

Terminator handlers are the final handlers in every HTTP handler chain. They receive a Result from previous handlers, check for errors, and format the final HTTP response:

import { primitives } from '@nodeblocks/backend-sdk';
import { Result } from 'neverthrow';

const { NodeblocksError, RouteHandlerPayload } = primitives;

export const normalizeUserTerminator = (
result: Result<RouteHandlerPayload, NodeblocksError>
) => {
if (result.isErr()) {
throw result.error;
}
const payload = result.value;
const { context } = payload;

if (!context.data?.user) {
throw new NodeblocksError(
500,
'Unknown error normalizing user',
'normalizeUserTerminator'
);
}
const { _id, ...user } = context.data.user;
return user;
};

Key Pattern: Terminator handlers always:

  1. Check result.isErr() and throw result.error for failed operations
  2. Extract payload with result.value and get data from payload.context.data
  3. Return the final formatted response object (not a Result type)

Note: Handlers can be either synchronous (RouteHandler) or asynchronous (AsyncRouteHandler) depending on whether they perform async operations like database calls or API requests.

WebSocket Handlers

Handlers for real-time WebSocket routes that return RxJS Subjects for bidirectional communication:

import { Subject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { primitives, types } from '@nodeblocks/backend-sdk';

const { WsRouteHandlerPayload } = types;

export const realtimeHandler = (payload: WsRouteHandlerPayload) => {
const subject = new Subject();

// Setup real-time data source
const changeStream = payload.context.db.data.watch();

changeStream.on('change', (changeDoc) => {
subject.next({
type: 'update',
data: changeDoc.fullDocument,
timestamp: Date.now()
});
});

// Cleanup when connection closes
subject.subscribe({
complete: () => changeStream.close(),
error: () => changeStream.close()
});

return subject;
};

🔄 Payload Structure

Every handler receives a payload object with the following structure:

interface HandlerPayload {
params: {
requestBody?: Record<string, any>; // POST/PATCH request body
requestParams?: Record<string, any>; // URL parameters (:userId)
requestQuery?: Record<string, any>; // Query string parameters
};
context: {
db: any;
mailService?: MailService;
// request: express.Request; disabled
[entityName: string]: any;
};
logger?: Logger;
[prop: string]: any;
}

🧑‍💻 Complete Handler Chain Example

Here's how handlers and terminator handlers work together in a complete chain:

import { primitives, handlers } from '@nodeblocks/backend-sdk';
import { ok, err, Result } from 'neverthrow';

const { AsyncRouteHandler, RouteHandlerPayload, NodeblocksError, lift } = primitives;
const { mergeData } = handlers;

// Handler: Database operation (returns Result)
export const getUserById: AsyncRouteHandler<
Result<RouteHandlerPayload, NodeblocksError>
> = async (payload) => {
const { context, params } = payload;
const userId = context.data?.userId || params.requestParams?.userId;

if (!userId) {
return err(new NodeblocksError(400, 'User ID is required', 'getUserById'));
}

try {
const user = await context.db.users.findOne({ id: String(userId) });
if (!user) {
return err(new NodeblocksError(404, 'User not found', 'getUserById'));
}
return ok(mergeData(payload, { user }));
} catch (_error) {
return err(new NodeblocksError(500, 'Failed to get user', 'getUserById'));
}
};

// Terminator handler: Format final response (handles Result, returns formatted data)
export const sanitizeUserTerminator = (result: Result<RouteHandlerPayload, NodeblocksError>) => {
if (result.isErr()) {
throw result.error;
}
const payload = result.value;
const { context } = payload;

if (!context.data?.user) {
throw new NodeblocksError(500, 'User data not available', 'sanitizeUserTerminator');
}

return {
id: context.data.user.id,
email: context.data.user.email
};
};

// How they're used together in a route
export const getUserRoute = withRoute({
method: 'GET',
path: '/users/:userId',
handler: compose(
getUserById, // Handler: gets user from DB
lift(sanitizeUserTerminator) // Terminator: formats final response
),
});

The handler component works closely with several utility functions:


➡️ Next

Learn about Schema » to see how data is shaped, or explore WebSocket Services » for real-time handler patterns.