🛣️ 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)
- Define — export
withRoute({ method, path, validators, handler })fromsrc/routes/<domain>.ts - Pair with schema —
compose(schema, route)insrc/features/<domain>.ts(schema setswithNextRoute) - Bundle — combine features in a service factory
- 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
| Protocol | Purpose | Final handler output |
|---|---|---|
http | REST API endpoints | Plain JSON data or { statusCode, data } after terminator |
ws | Real-time WebSocket connections | RxJS 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
statusCodeproperty —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
| Verb | Purpose |
|---|---|
GET | Read/retrieve resources |
POST | Create new resources |
PUT | Replace or full update resources |
PATCH | Partially update resources |
DELETE | Remove 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:
- Blocks or handlers — perform business logic (intermediate steps return
Result) - Terminator —
lift(orThrow(...))for block routes, orlift(*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 redactionwithPagination— auto-pagination onfindquerieswithPaginatedProperty— pagination on nested array propertieswithSoftDelete— 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
| Aspect | HTTP Routes | WebSocket Routes |
|---|---|---|
| Protocol | 'http' (default) | 'ws' |
| Method | Required (GET, POST, etc.) | Defaults to GET internally |
| Handler output | Plain data or { statusCode, data } | RxJS Subject |
| Communication | Request-response | Bidirectional streaming |
| Mounting | defService(feature) | defService(feature, wss) |
📐 Good Practices
- Path semantics — nouns for resources (
/identities), sub-paths for actions (/identities/:id/lock). - Complete Result chains — end Result-based chains with a terminator (
orThrowor*Terminator). - Prefer blocks — use parameter-driven blocks for new routes; handlers for legacy domains only.
- Keep logic small — prefer multiple composables to a single monolith.
- Validators first — lightweight checks before DB calls.
- 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:
| Module | Description | Reference |
|---|---|---|
| Address | Address lookup endpoints | SDK only — docs pending |
| Attributes | Attribute management routes | Attribute Routes » |
| Authentication | Login, logout, MFA, tokens | Authentication Routes » |
| Category | Category CRUD routes | Category Routes » |
| Chat | Channels, messages, subscriptions (includes WS) | Chat Routes » |
| Identity | Identity lifecycle routes | Identity Routes » |
| Invitation | Invitation management routes | Invitation Routes » |
| Location | Location management routes | Location Routes » |
| Notification | Notification endpoints | SDK only — docs pending |
| OAuth | Google, Twitter, LINE OAuth routes | OAuth Routes » |
| Order | Order management routes | Order Routes » |
| Organization | Organization routes | Organization Routes » |
| Product | Product management routes | Product Routes » |
| Profile | Profile and social routes | Profile Routes » |
➡️ Next
Learn about Feature » to compose routes with schemas, Schema » for validation, or explore Blocks » and Handler » for handler chain patterns.