⚙️ Handler
A handler is a function that receives the full route payload (params, context, optional logger) and implements business logic for an API operation. Operational handlers commonly return Result values — but they read and write the payload directly instead of receiving isolated arguments. Terminators consume those results and return the final response shape.
There is no handler registration API — a handler is simply an exported function in the SDK handlers module, composed directly in route chains.
Migration note: The SDK is moving toward parameter-driven blocks wired with
applyPayloadArgs. Many handlers inauthenticationandprofileare marked@deprecated migrate this to block. Prefer blocks for new routes; handlers remain in active use for category, attributes, invitation, order, and parts of chat, organization, product, and authentication.
🔍 Handler vs Block
| Aspect | Block | Handler |
|---|---|---|
| Input | Extracted parameters only | Full RouteHandlerPayload |
| Wiring | applyPayloadArgs(block, paths, key) | Passed directly to compose() |
| Testability | Pass plain arguments | Requires payload fixtures |
Block: getIdentityById(db, identityId) → Result<identity, Error>
Handler: getCategoryById(payload) → Result<payload with context.data, Error>
Use blocks for parameter-driven logic in new routes. Use handlers when maintaining legacy routes that need direct payload access. See Blocks » for the preferred pattern.
📋 Handler Lifecycle
handlers/*.ts → routes/*.ts → features/*.ts → defService()
(define) (compose) (schema + route) (serve)
- Define — export a handler function in
src/handlers/<domain>.ts - Wire — compose it into a route handler in
src/routes/<domain>.ts - Validate — pair the route with a schema in
src/features/<domain>.ts - Serve — mount the feature with
defService(feature)
🧑💻 Defining an HTTP Handler
HTTP operation handlers perform database calls, validation, transformations, etc. and commonly return Result<RouteHandlerPayload, Error>. NodeblocksError is the typical error class:
import { ok, err, Result } from 'neverthrow';
import { handlers, primitives, utils } from '@nodeblocks/backend-sdk';
const { mergeData } = handlers;
const { createBaseEntity } = utils;
// Async handler — pattern from handlers/category.ts
export const createCategory: primitives.AsyncRouteHandler<
Result<primitives.RouteHandlerPayload, Error>
> = async (payload) => {
const { params, context } = payload;
if (!params.requestBody || Object.keys(params.requestBody).length === 0) {
return err(
new primitives.NodeblocksError(400, 'Request body is required', 'createCategory')
);
}
const baseEntity = createBaseEntity(params.requestBody);
try {
const createdCategory = await context.db.categories.insertOne(baseEntity);
if (!createdCategory.insertedId) {
return err(
new primitives.NodeblocksError(400, 'Failed to create category', 'createCategory')
);
}
return ok(mergeData(payload, { categoryId: baseEntity.id }));
} catch {
return err(
new primitives.NodeblocksError(500, 'Failed to create category', 'createCategory')
);
}
};
Handlers can be synchronous (RouteHandler) or asynchronous (AsyncRouteHandler).
mergeData
mergeData (exported from handlers) uses Ramda mergeDeepRight to merge values into payload.context.data. Existing keys are preserved:
return ok(mergeData(payload, { categoryId: baseEntity.id }));
// Later step reads: context.data?.categoryId
Handlers used with flatMapAsync must wrap the return in ok() (or err() on failure). mergeData is also used internally by applyPayloadArgs in block routes.
See Composition Utilities » for full mergeData documentation.
🏁 Terminator Handlers
Terminator handlers are the final step in a handler chain. They receive a Result from previous handlers, throw on error, and return plain response data (not a Result).
Normalize terminator
Returns a formatted response object:
import { primitives } from '@nodeblocks/backend-sdk';
import { Result } from 'neverthrow';
// Pattern from handlers/category.ts — normalizeCategoryTerminator
export const normalizeCategoryTerminator = (
result: Result<primitives.RouteHandlerPayload, Error>
) => {
if (result.isErr()) {
throw result.error;
}
const { context } = result.value;
if (!context.data?.category) {
throw new primitives.NodeblocksError(
500,
'Unknown error normalizing category',
'normalizeCategoryTerminator'
);
}
const { _id, ...category } = context.data.category;
return category;
};
Status-code terminator
Returns a structured HTTP response with a status code:
// Pattern from handlers/category.ts — deleteCategoryTerminator
export const deleteCategoryTerminator = (
result: Result<primitives.RouteHandlerPayload, Error>
): { statusCode: number } => {
if (result.isErr()) {
throw result.error;
}
if (!result.value.context.data?.deleteCategory) {
throw new primitives.NodeblocksError(
500,
'Unknown error deleting category',
'deleteCategoryTerminator'
);
}
return { statusCode: 204 };
};
Terminator pattern:
- Check
result.isErr()andthrow result.error- Read data from
result.value.context.data- Return the final formatted object or
{ statusCode, data }— not aResult
For new routes, prefer lift(orThrow(...)) over custom terminators — see Route » and Blocks ».
🔌 WebSocket Handlers
The SDK defines WsRouteHandler and AsyncWsRouteHandler types in primitives — functions that return an RxJS Subject. defService supports WebSocket routes when passed a WebSocketServer as the second argument.
In practice, the SDK implements WebSocket streaming via blocks, not handlers. The chat message stream route uses streamChatMessages from blocks/chat/message.ts (returns Result<Subject<T>, Error>), wired with applyPayloadArgs and ending with lift(orThrow(...)).
For custom WebSocket handlers, the types are:
type WsRouteHandler<T = any> = (wsPayload: WsRouteHandlerPayload) => Subject<T>;
type AsyncWsRouteHandler<T = any> = (wsPayload: WsRouteHandlerPayload) => Promise<Subject<T>>;
defService injects params.requestQuery (parsed from the WebSocket URL) at connection time, even though WsRouteHandlerPayload in the type definition does not include params.
See Creating a WebSocket Service » for full WebSocket patterns.
🔄 Payload Structure
HTTP handlers receive primitives.RouteHandlerPayload:
type RouteHandlerPayload = {
params: {
requestBody?: Record<string, any>;
requestParams?: Record<string, any>;
requestQuery?: Record<string, any>;
};
context: ServiceContext;
logger?: Logger;
[prop: string]: unknown;
};
ServiceContext includes db, optional drivers (mailService, fileStorageDriver, OAuth drivers, findAddressDriver), authenticate, and configuration. The [entityName: string]: any index allows additional fields.
defService injects at runtime for HTTP routes:
context.request— Express requestcontext.response— Express responsecontext.data— accumulated bymergeData/applyPayloadArgsduring the handler chain
🧑💻 Complete Handler Chain Example
From routes/category.ts — handlers composed with a terminator. Validators run before the handler chain; withLogging wraps steps for observability:
import { handlers, primitives, validators } from '@nodeblocks/backend-sdk';
const {
createCategory,
getCategoryById,
normalizeCategoryTerminator,
} = handlers;
const { compose, flatMapAsync, lift, withLogging, withRoute } = primitives;
const { isAuthenticated, checkIdentityType } = validators;
export const createCategoryRoute = withRoute({
method: 'POST',
path: '/categories',
validators: [isAuthenticated(), checkIdentityType(['admin'])],
handler: compose(
withLogging(createCategory),
flatMapAsync(withLogging(getCategoryById)),
lift(withLogging(normalizeCategoryTerminator))
),
});
Chain flow:
- Validators —
isAuthenticated,checkIdentityTyperun before any handler work createCategory— readsparams.requestBody, inserts into DB, storescategoryIdviamergeDatagetCategoryById— readscontext.data.categoryId, fetches category, storescategoryviamergeDatanormalizeCategoryTerminator— strips internal fields and returns the HTTP response
Use flatMapAsync between async handlers that return Result.
📦 SDK Handler Modules
Handlers are organized by domain in the SDK handlers namespace, matching src/handlers/index.ts:
| Module | Description | Reference |
|---|---|---|
| Attributes | Attribute management handlers | Attribute Handlers » |
| Authentication | Auth handlers (many deprecated — migrating to blocks) | Authentication Handlers » |
| Category | Category CRUD handlers | Category Handlers » |
| Chat | Chat channel, message, subscription handlers | Chat Handlers » |
| Invitation | Invitation management handlers | Invitation Handlers » |
| Order | Order management handlers | Order Handlers » |
| Organization | Organization handlers | Organization Handlers » |
| Product | Product management handlers | Product Handlers » |
| Profile | Profile handlers (many deprecated — migrating to blocks) | SDK only — docs pending |
| Utils | mergeData utility | Used by all handlers and applyPayloadArgs |
🛠️ Related Utilities
- Composition Utilities —
compose,flatMapAsync,lift,mergeData - Entity Utilities —
createBaseEntityand entity management - Blocks » — parameter-driven functions wired with
applyPayloadArgs(preferred for new routes)
➡️ Next
Learn about Schema » to see how data is shaped, or explore Route » for composing handlers and blocks in routes.