๐ 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
composewithout global state - Dependencies injected via
partialwhen mounting withdefService - 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)
- Define โ create
withSchemaandwithRoutecomposers insrc/schemas/andsrc/routes/ - Compose โ export
compose(schema, route)fromsrc/features/<domain>.ts - Bundle โ combine features in a service factory (
src/services/) - Serve โ mount with
defService(partial(compose(...), [deps])); pass a WebSocket server as the seconddefServiceargument 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:
withSchemasetsservice.withNextRouteโ a function that wraps the next route handler with validationwithRouteconsumeswithNextRoutewhen registering the route, then clears it- 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:
partial(compose(...features), [deps])pre-appliesdataStores,configuration,authenticate, and optional non-WebSocket drivers as the initialServiceDefinition- Each feature composer adds routes (and schema metadata) to that definition
defServiceturns the finalServiceDefinitioninto 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.identitiesServicefactory with the same composition pattern.
๐ Good Practicesโ
- One operation per feature โ create separate features for create, read, update, delete.
- Name consistently โ
<verb><Entity>Featurehelps discovery (getIdentityFeature,findIdentitiesFeature). - Schema before route โ validation must precede route registration due to
withNextRoute. - 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:
| Module | Description | Reference |
|---|---|---|
| Address | Address lookup endpoints | SDK only โ docs pending |
| Attributes | Attribute management | Attribute Features ยป |
| Authentication | Login, logout, MFA, tokens | Authentication Features ยป |
| Category | Category CRUD | Category Features ยป |
| Chat | Channels, messages, subscriptions | Chat Features ยป |
| Identity | Identity lifecycle (CRUD, lock/unlock) | Identity Features ยป |
| Invitation | Invitation management | Invitation Features ยป |
| Location | Hierarchical location management | Location Features ยป |
| Notification | Notification endpoints | SDK only โ docs pending |
| OAuth | Google, Twitter, LINE OAuth flows | OAuth Features ยป |
| Order | Order management | Order Features ยป |
| Organization | Organization and workspace logic | Organization Features ยป |
| Product | Product management | Product Features ยป |
| Profile | Profile, avatars, social engagement | Profile 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.