Skip to main content
Version: 0.13.0 (Previous)

๐Ÿ›ฃ๏ธ Route

A route bundles a protocol, HTTP method, path, validators, and a handler chain. The withRoute helper returns a RouteComposer โ€” a function that adds route metadata to a ServiceDefinition. Routes do not plug into Express directly; they are composed into features and mounted via defService.

Routes support both HTTP (default) and WebSocket (protocol: 'ws') protocols.


๐Ÿ” What is a Route?โ€‹

withRoute returns (service: ServiceDefinition) => ServiceDefinition. It appends (or replaces) a route in service.routes. If a prior withSchema set service.withNextRoute, validation is injected into the route handler when the route is registered.

Express wiring happens when defService(adapter) runs โ€” it builds an Express router, registers HTTP routes, and attaches WebSocket handlers to a provided WebSocketServer.

Route lifecycleโ€‹

routes/*.ts โ†’ features/*.ts โ†’ services/*.ts โ†’ defService()
(withRoute) (compose schema + route) (compose features) (express.Router)
  1. Define โ€” export withRoute({ method, path, validators, handler }) from src/routes/<domain>.ts
  2. Pair with schema โ€” compose(schema, route) in src/features/<domain>.ts (schema sets withNextRoute)
  3. Bundle โ€” combine features in a service factory
  4. Serve โ€” defService(partial(compose(...features), [deps])) produces the Express router

withRoute replaces existing routes with the same method + path + protocol. See Feature ยป and Schema ยป for schema pairing.


๐Ÿ“ RouteConfigโ€‹

withRoute accepts Partial<RouteConfig> so it can merge with a preceding composer. It fills in protocol: 'http' when omitted and fills in method: 'GET' for WebSocket routes. For HTTP routes, you must provide method, path, and handler; withRoute does not invent a default HTTP method. The resulting stored route has the required fields shown below:

interface RouteConfig {
protocol: 'http' | 'ws';
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
path: string;
validators?: Validator[] | WsValidator[];
schema?: SchemaDefinition; // legacy โ€” typically applied via withNextRoute from withSchema
openapi?: OpenAPIOperation; // typically applied via withNextRoute from withSchema
handler:
| AsyncRouteHandler
| RouteHandler
| AsyncWsRouteHandler
| WsRouteHandler;
}

Protocolโ€‹

ProtocolPurposeFinal handler output
httpREST API endpointsPlain JSON data or { statusCode, data } after terminator
wsReal-time WebSocket connectionsRxJS Subject (via block chain or direct handler)

In a Result-based chain, intermediate steps return Result values. The terminator (orThrow or custom *Terminator) produces the value defService sends to the client.

HTTP response handlingโ€‹

defService processes the final handler result:

  • If the result has a statusCode property โ€” res.status(statusCode).json(data) (e.g. { statusCode: 204 } for deletions)
  • Otherwise โ€” res.json(result) with plain response data

defService also adds express.json() to the router and applies any service.middleware entries.

Methodโ€‹

VerbPurpose
GETRead/retrieve resources
POSTCreate new resources
PUTReplace or full update resources
PATCHPartially update resources
DELETERemove resources

Pathโ€‹

Express-style template. Use plural nouns for collections (/identities) and sub-paths for actions (/identities/:identityId/lock). For HTTP routes, dynamic parts become params.requestParams.


โœ… Validatorsโ€‹

Async hooks that run before any handler work. They receive the same payload object (RouteHandlerPayload for HTTP, WsRouteHandlerPayload for WebSocket).

import { primitives } from '@nodeblocks/backend-sdk';

export const validateEmail: primitives.Validator = async ({ params }) => {
if (!params.requestBody?.email?.includes('@')) {
throw new primitives.NodeblocksError(400, 'Invalid email', 'validateEmail');
}
};

The idiomatic way to reject a request is to throw new NodeblocksError(status, message). HTTP validators must return Promise<void>; WebSocket validators may return void or Promise<void>.


๐Ÿ”— Handler Chainsโ€‹

Routes delegate business logic to composed handler chains:

  1. Blocks or handlers โ€” perform business logic (intermediate steps return Result)
  2. Terminator โ€” lift(orThrow(...)) for block routes, or lift(*Terminator) for legacy handler routes

Block-based routes (preferred)โ€‹

Prefer parameter-driven blocks with applyPayloadArgs for new routes (identity, authentication, organization, product):

import { blocks, primitives, validators } from '@nodeblocks/backend-sdk';

const { getIdentityById, normalizeIdentity } = blocks;
const { compose, flatMapAsync, withRoute, lift, applyPayloadArgs, orThrow } =
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']]))
),
});

For routes with typed block errors, map them in orThrow:

import { blocks } from '@nodeblocks/backend-sdk';

const { OrganizationBlockError } = blocks;

lift(
orThrow(
[[OrganizationBlockError, 500]],
[['context', 'data', 'organization']]
)
);

See Blocks ยป for the full block pattern.

Handler-based routes (legacy)โ€‹

Category, order, attributes, invitation, and parts of chat compose handlers.* directly with custom terminators:

import { handlers, primitives, validators } from '@nodeblocks/backend-sdk';

const { createCategory, getCategoryById, normalizeCategoryTerminator } = handlers;
const { compose, flatMapAsync, withRoute, lift, withLogging } = 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))
),
});

See Handler ยป for legacy handler patterns and migration notes.

Composition patternsโ€‹

Block chain:

handler: compose(
applyPayloadArgs(block1, [/* paths */], 'result1'),
flatMapAsync(applyPayloadArgs(block2, [/* paths */], 'result2')),
lift(orThrow([[BlockError1, 404], [BlockError2, 500]], [['context', 'data', 'result2']]))
)

Handler chain:

handler: compose(
handler1,
flatMapAsync(handler2),
lift(terminatorHandler)
)

Route wrappersโ€‹

Common combinators applied around handler steps:

  • withLogging โ€” structured logging with redaction
  • withPagination โ€” auto-pagination on find queries
  • withPaginatedProperty โ€” pagination on nested array properties
  • withSoftDelete โ€” transparent soft-delete filtering on reads/writes

๐Ÿ”Œ WebSocket Routesโ€‹

WebSocket routes use protocol: 'ws'. Pass a WebSocketServer as the second argument to defService:

defService(chatFeature, wss);

defService injects params.requestQuery (parsed from the WebSocket URL) at connection time.

SDK pattern: block compose chainโ€‹

The production WebSocket route in the SDK (streamChatMessagesRoute) uses a block compose chain that ends by extracting an RxJS Subject from context.data:

import { blocks, primitives } from '@nodeblocks/backend-sdk';

const { streamChatMessages, normalizeChatMessageStream, ChatMessageBadRequestError, ChatMessageUnknownError } = blocks;
const { compose, applyPayloadArgs, flatMapAsync, lift, orThrow, withLogging, withRoute } = primitives;

export const streamChatMessagesRoute = withRoute({
protocol: 'ws',
path: '/messages/listen',
handler: compose(
withLogging(
applyPayloadArgs(
streamChatMessages,
[
['context', 'db', 'chatMessages'],
['params', 'requestQuery', 'channelId'],
],
'streamSubject'
)
),
flatMapAsync(
withLogging(
applyPayloadArgs(
normalizeChatMessageStream,
[
['context', 'fileStorageDriver'],
['context', 'data', 'streamSubject'],
],
'normalizedStreamSubject'
)
)
),
lift(
withLogging(
orThrow(
[
[ChatMessageBadRequestError, 400],
[ChatMessageUnknownError, 500],
],
[['context', 'data', 'normalizedStreamSubject']]
)
)
)
),
});

Custom WebSocket handlersโ€‹

The SDK also defines WsRouteHandler and AsyncWsRouteHandler types for handlers that return an RxJS Subject directly. Use these for custom implementations outside the block-chain pattern.

See Creating a WebSocket Service ยป for full WebSocket patterns.

WebSocket vs HTTPโ€‹

AspectHTTP RoutesWebSocket Routes
Protocol'http' (default)'ws'
MethodRequired (GET, POST, etc.)Defaults to GET internally
Handler outputPlain data or { statusCode, data }RxJS Subject
CommunicationRequest-responseBidirectional streaming
MountingdefService(feature)defService(feature, wss)

๐Ÿ“ Good Practicesโ€‹

  1. Path semantics โ€” nouns for resources (/identities), sub-paths for actions (/identities/:id/lock).
  2. Complete Result chains โ€” end Result-based chains with a terminator (orThrow or *Terminator).
  3. Prefer blocks โ€” use parameter-driven blocks for new routes; handlers for legacy domains only.
  4. Keep logic small โ€” prefer multiple composables to a single monolith.
  5. Validators first โ€” lightweight checks before DB calls.
  6. Stateless โ€” routes should not mutate global state; everything arrives via payload.

๐Ÿ“ฆ SDK Route Modulesโ€‹

Routes are organized by domain in the SDK routes namespace, matching src/routes/index.ts:

ModuleDescriptionReference
AddressAddress lookup endpointsSDK only โ€” docs pending
AttributesAttribute management routesAttribute Routes ยป
AuthenticationLogin, logout, MFA, tokensAuthentication Routes ยป
CategoryCategory CRUD routesCategory Routes ยป
ChatChannels, messages, subscriptions (includes WS)Chat Routes ยป
IdentityIdentity lifecycle routesIdentity Routes ยป
InvitationInvitation management routesInvitation Routes ยป
LocationLocation management routesLocation Routes ยป
NotificationNotification endpointsSDK only โ€” docs pending
OAuthGoogle, Twitter, LINE OAuth routesOAuth Routes ยป
OrderOrder management routesOrder Routes ยป
OrganizationOrganization routesOrganization Routes ยป
ProductProduct management routesProduct Routes ยป
ProfileProfile and social routesProfile Routes ยป

โžก๏ธ Nextโ€‹

Learn about Feature ยป to compose routes with schemas, Schema ยป for validation, or explore Blocks ยป and Handler ยป for handler chain patterns.