Skip to main content
Version: 0.14.0 (Latest)

๐Ÿš€ Feature

A feature is a naming convention for pairing request validation with a single route. In the SDK, each feature is compose(withSchema(...), withRoute(...)) โ€” a Composable that transforms a ServiceDefinition, not an Express router.

There is no defFeature registration API. Features are exported from src/features/ and combined into services via compose, then mounted with defService.


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

Each exported feature pairs one schema composer with one route composer. Multiple endpoints require multiple features composed in a service:

// One feature = one schema + one route
export const getIdentityFeature = compose(getIdentitySchema, getIdentityRoute);

Services combine many features:

compose(getIdentityFeature, findIdentitiesFeature, updateIdentityFeature, ...)

Import features via the single namespace export:

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

// From the features namespace (identity module):
// getIdentityFeature, findIdentitiesFeature, updateIdentityFeature,
// deleteIdentityFeature, lockIdentityFeature, unlockIdentityFeature

const { getIdentityFeature, findIdentitiesFeature } = features;

Why use features:

  • Bundles validation and routing into a reusable unit
  • Composable into services via compose without global state
  • Dependencies injected via partial when mounting with defService
  • Replaceable in custom services by composing different features

Feature lifecycleโ€‹

schemas/*.ts + routes/*.ts โ†’ features/*.ts โ†’ services/*.ts โ†’ defService()
(withSchema/withRoute) (compose) (compose many) (router)
  1. Define โ€” create withSchema and withRoute composers in src/schemas/ and src/routes/
  2. Compose โ€” export compose(schema, route) from src/features/<domain>.ts
  3. Bundle โ€” combine features in a service factory (src/services/)
  4. Serve โ€” mount with defService(partial(compose(...), [deps])); pass a WebSocket server as the second defService argument when needed

โš™๏ธ How It Worksโ€‹

A feature is a function (service: ServiceDefinition) => ServiceDefinition. Under the hood, primitives.compose is Ramda pipe.

Schema attachment uses the withNextRoute hook:

  1. withSchema sets service.withNextRoute โ€” a function that wraps the next route handler with validation
  2. withRoute consumes withNextRoute when registering the route, then clears it
  3. Schema must come before route in compose(...)
// Schema is composed BEFORE the route it validates
export const getIdentityFeature = compose(getIdentitySchema, getIdentityRoute);

For HTTP routes, validation runs before the route handler. Invalid requests throw NodeblocksError with status 400 before handlers execute. WebSocket schemas validate client messages and signal invalid messages through the stream instead.

See Schema ยป for withSchema details and Route ยป for withRoute details.


๐Ÿง‘โ€๐Ÿ’ป Defining a Featureโ€‹

Features are plain compose exports โ€” no registration step. Import the composition primitive from the SDK; the route and schema composers are the ones defined by your application:

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

import { getIdentityRoute } from './routes';
import { getIdentitySchema } from './schemas';

const { compose } = primitives;

export const getIdentityFeature = compose(getIdentitySchema, getIdentityRoute);

A feature returns a Composable, not an Express router. Use defService to produce a router.


๐Ÿ”€ Conditional Schemasโ€‹

Some features select a schema at compose time based on service configuration. Auth features use ifElse and match from primitives to branch on configuration.authMode:

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

import { logoutRoute } from './routes';
import { logoutBearerSchema, logoutCookieSchema } from './schemas';

const { compose, ifElse, match } = primitives;
const { isCookieMode } = utils;

export const logoutFeature = compose(
ifElse(
match(isCookieMode, ['configuration', 'authMode']),
logoutCookieSchema,
logoutBearerSchema
),
logoutRoute
);

The same pattern is used in refreshTokenFeature. When authMode is 'cookie', the cookie schema is composed; otherwise the bearer schema is used.


๐Ÿง‘โ€๐Ÿ’ป Using Features in a Serviceโ€‹

Features are consumed through defService. Use Ramda partial to pre-apply dependencies as the starting ServiceDefinition:

import { partial } from 'ramda';
import { Collection } from 'mongodb';

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

const {
getIdentityFeature,
findIdentitiesFeature,
updateIdentityFeature,
deleteIdentityFeature,
lockIdentityFeature,
unlockIdentityFeature,
} = features;
const { compose, defService } = primitives;
const { getBearerTokenInfo, getCookieTokenInfo } = utils;

export const identitiesService = (
dataStores: { identities: Collection },
configuration: {
authSecrets: { authEncSecret: string; authSignSecret: string };
authMode?: 'bearer' | 'cookie';
}
) => {
return defService(
partial(
compose(
getIdentityFeature,
findIdentitiesFeature,
updateIdentityFeature,
deleteIdentityFeature,
lockIdentityFeature,
unlockIdentityFeature
),
[
{
dataStores,
configuration,
authenticate:
configuration.authMode === 'cookie'
? getCookieTokenInfo
: getBearerTokenInfo,
},
]
)
);
};

How partial + defService work:

  1. partial(compose(...features), [deps]) pre-applies dataStores, configuration, authenticate, and optional non-WebSocket drivers as the initial ServiceDefinition
  2. Each feature composer adds routes (and schema metadata) to that definition
  3. defService turns the final ServiceDefinition into an Express router

Commonly injected fields: dataStores, configuration, and authenticate. Optional drivers include mailService, fileStorageDriver, OAuth drivers (googleOAuthDriver, twitterOAuthDriver, lineOAuthDriver), and findAddressDriver.

WebSocket routes are the exception: webSocketServer is not placed in the service context. Pass it as the second argument to defService; the built-in chatService accepts it in its third drivers argument and forwards it to defService.

When using cookie authentication (authMode: 'cookie'), register the cookie-parser Express middleware before mounting the service router.

The SDK ships a ready-made services.identitiesService factory with the same composition pattern.


๐Ÿ“ Good Practicesโ€‹

  1. One operation per feature โ€” create separate features for create, read, update, delete.
  2. Name consistently โ€” <verb><Entity>Feature helps discovery (getIdentityFeature, findIdentitiesFeature).
  3. Schema before route โ€” validation must precede route registration due to withNextRoute.
  4. Compose many features in services โ€” a service is compose(featureA, featureB, ...), not one feature with many routes.

๐Ÿ“ฆ SDK Feature Modulesโ€‹

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

ModuleDescriptionReference
AddressAddress lookup endpointsSDK only โ€” docs pending
AttributesAttribute managementAttribute Features ยป
AuthenticationLogin, logout, MFA, tokensAuthentication Features ยป
CategoryCategory CRUDCategory Features ยป
ChatChannels, messages, subscriptionsChat Features ยป
IdentityIdentity lifecycle (CRUD, lock/unlock)Identity Features ยป
InvitationInvitation managementInvitation Features ยป
LocationHierarchical location managementLocation Features ยป
NotificationNotification endpointsSDK only โ€” docs pending
OAuthGoogle, Twitter, LINE OAuth flowsOAuth Features ยป
OrderOrder managementOrder Features ยป
OrganizationOrganization and workspace logicOrganization Features ยป
ProductProduct managementProduct Features ยป
ProfileProfile, avatars, social engagementProfile Features ยป

Ready-made service factories that compose these features are available in the services namespace. See Service ยป.


โžก๏ธ Nextโ€‹

Learn about Service ยป for ready-made service factories, Schema ยป for validation, or Route ยป for handler wiring.