Skip to main content
Version: 0.14.0 (Latest)

๐Ÿงฉ 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 applyPayloadArgs to 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 Result types; orThrow maps 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 from src/blocks/ and wire it in a route.

Block lifecycleโ€‹

blocks/*.ts โ†’ routes/*.ts โ†’ features/*.ts โ†’ defService()
(define) (wire) (schema + route) (serve)
  1. Define โ€” export a function in src/blocks/<domain>.ts
  2. Wire โ€” compose it into a route handler with applyPayloadArgs 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 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โ€‹

BehaviorDetail
Argument extractionEach path tuple (e.g. ['context', 'db', 'identities']) is resolved from the payload via Ramda path
Optional keyWhen provided, the block result is stored at context.data[key]. When omitted, the entire return value is merged into context.data
Sync and asyncBoth sync and async blocks are supported โ€” non-Promise returns are handled automatically
Result propagationIf 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 โ€” NodeblocksError with HTTP status codes baked in (exception to the BlockError pattern)

Block chains end with lift(orThrow(errorMap, successMap)):

  • errorMap โ€” maps BlockError subclasses to HTTP status codes
  • successMap โ€” 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:

ModuleAdditional 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โ€‹

AspectBlockHandler
InputExtracted parameters onlyFull RouteHandlerPayload
WiringapplyPayloadArgs(block, paths, key)Passed directly to compose()
TestabilityPass plain argumentsRequires 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:

ModuleDescriptionReference
AddressAddress lookup and normalizationSDK only โ€” docs pending
AuthenticationAuthentication and token logicAuthentication Blocks ยป
AvatarAvatar normalization and file managementAvatar Blocks ยป
ChatMessaging and real-time communicationChat Blocks ยป
CommonShared block utilitiesCommon Blocks ยป
File StorageSecure file operations and signed URLsFile Storage Blocks ยป
IdentityIdentity lifecycle and security managementIdentity Blocks ยป
LocationHierarchical location managementLocation Blocks ยป
MongoMongoDB database operations and utilitiesMongo Blocks ยป
NotificationNotification creation and deliverySDK only โ€” docs pending
OAuthThird-party OAuth provider integrationOAuth Blocks ยป
OrderOrder querying and filteringOrder Blocks ยป
OrganizationOrganization and workspace logicOrganization Blocks ยป
ProductProduct management operationsProduct Blocks ยป
ProfileProfile relationships and social engagementProfile Blocks ยป
UtilsRedirect helpers and password generationSDK 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.