Skip to main content
Version: 🚧 Canary

⚙️ 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 in authentication and profile are 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

AspectBlockHandler
InputExtracted parameters onlyFull RouteHandlerPayload
WiringapplyPayloadArgs(block, paths, key)Passed directly to compose()
TestabilityPass plain argumentsRequires 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)
  1. Define — export a handler function in src/handlers/<domain>.ts
  2. Wire — compose it into a route handler in src/routes/<domain>.ts
  3. Validate — pair the route with a schema in src/features/<domain>.ts
  4. 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:

  1. Check result.isErr() and throw result.error
  2. Read data from result.value.context.data
  3. Return the final formatted object or { statusCode, data } — not a Result

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 request
  • context.response — Express response
  • context.data — accumulated by mergeData / applyPayloadArgs during 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:

  1. ValidatorsisAuthenticated, checkIdentityType run before any handler work
  2. createCategory — reads params.requestBody, inserts into DB, stores categoryId via mergeData
  3. getCategoryById — reads context.data.categoryId, fetches category, stores category via mergeData
  4. normalizeCategoryTerminator — 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:

ModuleDescriptionReference
AttributesAttribute management handlersAttribute Handlers »
AuthenticationAuth handlers (many deprecated — migrating to blocks)Authentication Handlers »
CategoryCategory CRUD handlersCategory Handlers »
ChatChat channel, message, subscription handlersChat Handlers »
InvitationInvitation management handlersInvitation Handlers »
OrderOrder management handlersOrder Handlers »
OrganizationOrganization handlersOrganization Handlers »
ProductProduct management handlersProduct Handlers »
ProfileProfile handlers (many deprecated — migrating to blocks)SDK only — docs pending
UtilsmergeData utilityUsed by all handlers and applyPayloadArgs


➡️ Next

Learn about Schema » to see how data is shaped, or explore Route » for composing handlers and blocks in routes.