๐งฉ Block
Blocks are parameter-driven business logic functions that contain the core application logic for specific operations. They are designed to be reusable, testable, and composable building blocks that separate business logic from payload handling and routing concerns.
There is no block registration API โ a block is simply an exported function in the SDK blocks module, wired into routes via primitives.applyPayloadArgs.
๐ What is a Block?โ
A block receives only the data it needs โ not the full RouteHandlerPayload. Most domain blocks return a Result for explicit error handling; helper and terminator functions may return plain values or response shapes. Routes bridge payload and blocks with applyPayloadArgs, which extracts values from the payload and stores block results in context.data.
โโโโโโโโโโโโโโโ
โ Route โ compose(applyPayloadArgs(...), flatMapAsync(...), lift(orThrow(...)))
โโโโโโโโโโโโโโโค
โ Block โท โ Parameter-driven business logic
โ โ getIdentityById(db, identityId)
โโโโโโโโโโโโโโโ
Design principles:
- Separation of concerns โ blocks contain business logic; routes use
applyPayloadArgsto bridge payload and blocks. - Parameter-driven โ blocks take only what they need via extracted arguments, not the full payload (easy to unit test).
- Error handling โ blocks return
Resulttypes;orThrowmaps domain errors to HTTP responses at the end of the chain. - Composability โ blocks are chained in route handler pipelines with
flatMapAsync. - Convention-based โ no
defBlock, registry, or metadata schema; export a function fromsrc/blocks/and wire it in a route.
Block lifecycleโ
blocks/*.ts โ routes/*.ts โ features/*.ts โ defService()
(define) (wire) (schema + route) (serve)
- Define โ export a function in
src/blocks/<domain>.ts - Wire โ compose it into a route handler with
applyPayloadArgsinsrc/routes/<domain>.ts - Validate โ pair the route with a schema in
src/features/<domain>.ts - Serve โ mount the feature with
defService(feature)
๐งโ๐ป Defining a Blockโ
Blocks are plain exported functions โ no registration step:
import { Collection } from 'mongodb';
import { err, ok } from 'neverthrow';
import { primitives } from '@nodeblocks/backend-sdk';
const { BlockError } = primitives;
class ThingBlockError extends BlockError {}
class ThingNotFoundBlockError extends ThingBlockError {}
// src/blocks/my-domain.ts โ application code, no registration required
export async function getCustomThingById(db: Collection, id: string) {
try {
const result = await db.findOne({ id: String(id) });
if (!result) {
return err(new ThingNotFoundBlockError('Thing not found'));
}
return ok(result);
} catch {
return err(new ThingBlockError('Failed to get thing'));
}
}
This example defines an application block; it is not an SDK export. SDK blocks are imported from the single blocks namespace, for example const { getIdentityById } = blocks.
๐งโ๐ป Using Blocks in Routesโ
Blocks are composed into routes with applyPayloadArgs. It takes a block function, path tuples to extract arguments from the payload, and an optional key to store the result in context.data:
import { blocks, primitives, validators } from '@nodeblocks/backend-sdk';
const { getIdentityById, normalizeIdentity } = blocks;
const { applyPayloadArgs, compose, flatMapAsync, lift, orThrow, withRoute } =
primitives;
const { isAuthenticated, checkIdentityType } = validators;
export const getIdentityRoute = withRoute({
method: 'GET',
path: '/identities/:identityId',
validators: [isAuthenticated(), checkIdentityType(['admin'])],
handler: compose(
applyPayloadArgs(
getIdentityById,
[
['context', 'db', 'identities'],
['params', 'requestParams', 'identityId'],
],
'rawIdentity'
),
flatMapAsync(
applyPayloadArgs(
normalizeIdentity,
[['context', 'data', 'rawIdentity']],
'identity'
)
),
lift(orThrow([], [['context', 'data', 'identity']]))
),
});
applyPayloadArgs(fn, paths, key?) returns an AsyncRouteHandler โ it does not take the payload as a second argument.
applyPayloadArgs behaviorโ
| Behavior | Detail |
|---|---|
| Argument extraction | Each path tuple (e.g. ['context', 'db', 'identities']) is resolved from the payload via Ramda path |
Optional key | When provided, the block result is stored at context.data[key]. When omitted, the entire return value is merged into context.data |
| Sync and async | Both sync and async blocks are supported โ non-Promise returns are handled automatically |
| Result propagation | If the block returns Result.err(), the chain short-circuits. Result.ok() value is merged into the payload |
With key (most common):
applyPayloadArgs(getIdentityById, [
['context', 'db', 'identities'],
['params', 'requestParams', 'identityId'],
], 'rawIdentity')
// โ context.data.rawIdentity = identity object
Without key (store the return value in context.data):
applyPayloadArgs(buildUpdatePayload, [
['params', 'requestBody'],
])
// โ return value stored in context.data
Sync block (e.g. normalizeIdentity):
applyPayloadArgs(normalizeIdentity, [['context', 'data', 'rawIdentity']], 'identity')
// normalizeIdentity is synchronous โ applyPayloadArgs handles it the same way
โ ๏ธ Error Handling & Terminatorsโ
Domain blocks commonly return Result<T, Error>. Helper and terminator functions may instead return plain values or response shapes. The error type depends on the domain:
- Most domains โ domain-specific
*BlockError extends BlockError(e.g.ProfileBlockError,AuthenticationBlockError) - Identity blocks โ
NodeblocksErrorwith HTTP status codes baked in (exception to theBlockErrorpattern)
Block chains end with lift(orThrow(errorMap, successMap)):
errorMapโ mapsBlockErrorsubclasses to HTTP status codessuccessMapโ extracts response data from payload paths (e.g.[['context', 'data', 'identity']])
lift(orThrow(
[[ProfileNotFoundBlockError, 404], [ProfileBlockError, 500]],
[['context', 'data', 'profile']]
))
When errorMap is empty ([]), unmatched errors are re-thrown as-is โ used when blocks already return NodeblocksError with status codes (identity routes).
See Route ยป for full terminator patterns.
๐ง Common Patternsโ
withLoggingโ
Production routes often wrap block steps for observability:
flatMapAsync(withLogging(applyPayloadArgs(getIdentityById, paths, 'rawIdentity')))
Block terminatorsโ
Some functions in the blocks module are handler-shaped terminators โ they take Result<RouteHandlerPayload, Error> and shape the HTTP response. Example: deleteIdentityTerminator returns { statusCode: 204 }.
compose(
applyPayloadArgs(deleteIdentity, paths, 'deleteIdentity'),
lift(deleteIdentityTerminator)
)
Prefer lift(orThrow(...)) for new routes; block terminators exist for legacy or special response shapes.
mapMatchingErrorToFalseโ
Authentication routes use this to convert specific block errors into ok(false) instead of failing the chain:
import { blocks, primitives } from '@nodeblocks/backend-sdk';
const { mapMatchingErrorToFalse } = primitives;
const { checkEmailIsUniqueInIdentities, AuthenticationConflictError } = blocks;
const safeCheck = mapMatchingErrorToFalse(checkEmailIsUniqueInIdentities, [
AuthenticationConflictError,
]);
// AuthenticationConflictError โ ok(false) instead of err(...)
โก Exceptions to Parameter-Driven Blocksโ
Most blocks receive only domain data (db, IDs, DTOs). A few modules also accept Express types, extracted via applyPayloadArgs:
| Module | Additional parameters |
|---|---|
OAuth (blocks/oauth/*) | Request, Response |
Authentication (blocks/authentication.ts) | Request (headers, fingerprint) |
Utils (blocks/utils.ts) | Response (redirectTo) |
These blocks are still wired the same way โ applyPayloadArgs extracts context.request or context.response from the payload.
๐ Block vs Handlerโ
| 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 |
Use blocks for parameter-driven logic; use handlers when the function needs direct access to multiple payload fields. See Handler ยป for full examples and WebSocket handlers.
๐ฆ SDK Block Modulesโ
Blocks are organized by domain in the SDK blocks namespace, matching src/blocks/index.ts:
| Module | Description | Reference |
|---|---|---|
| Address | Address lookup and normalization | SDK only โ docs pending |
| Authentication | Authentication and token logic | Authentication Blocks ยป |
| Avatar | Avatar normalization and file management | Avatar Blocks ยป |
| Chat | Messaging and real-time communication | Chat Blocks ยป |
| Common | Shared block utilities | Common Blocks ยป |
| File Storage | Secure file operations and signed URLs | File Storage Blocks ยป |
| Identity | Identity lifecycle and security management | Identity Blocks ยป |
| Location | Hierarchical location management | Location Blocks ยป |
| Mongo | MongoDB database operations and utilities | Mongo Blocks ยป |
| Notification | Notification creation and delivery | SDK only โ docs pending |
| OAuth | Third-party OAuth provider integration | OAuth Blocks ยป |
| Order | Order querying and filtering | Order Blocks ยป |
| Organization | Organization and workspace logic | Organization Blocks ยป |
| Product | Product management operations | Product Blocks ยป |
| Profile | Profile relationships and social engagement | Profile Blocks ยป |
| Utils | Redirect helpers and password generation | SDK only โ docs pending |
โก๏ธ Nextโ
Learn about Route ยป to see how blocks are composed into handler chains, or explore domain block references in the table above.