メインコンテンツまでスキップ
バージョン: 🚧 Canary

🧩 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 blocksNodeblocksError 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.