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

🛣️ 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 schemacompose(schema, route) in src/features/<domain>.ts (schema sets withNextRoute)
  3. Bundle — combine features in a service factory
  4. ServedefService(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. Terminatorlift(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.