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

🚀 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.