๐ง 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.
๐ Handler Typesโ
Nodeblocks provides two main handler types based on their role in the handler chain:
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 }));
};
Note: Handlers can be either synchronous (
RouteHandler
) or asynchronous (AsyncRouteHandler
) depending on whether they perform async operations like database calls or API requests.
Terminator Handlersโ
Terminator handlers are the final handlers in every 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:
- Check
result.isErr()
andthrow result.error
for failed operations- Extract payload with
result.value
and get data frompayload.context.data
- Return the final formatted response object (not a Result type)
๐ 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, RoureHandlerPayload, 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
),
});
๐ ๏ธ Related Utilitiesโ
The handler component works closely with several utility functions:
- Composition Utilities - For
mergeData
and function composition - Entity Utilities - For
createBaseEntity
and entity management
โก๏ธ Nextโ
Learn about Schema ยป to see how data is shaped.