đ ī¸ Service
A service is a naming convention for a factory function that composes domain features and mounts them as an Express router via defService. There is no service registration API â the SDK exports ready-made factories from src/services/.
Each factory returns express.Router with HTTP (and optionally WebSocket) endpoints. Services are not in-process domain APIs you call directly from application code.
đ What is a Service?â
A service bundles one or more features â each feature is compose(withSchema(...), withRoute(...)) â into a single mountable router:
schemas/*.ts + routes/*.ts â features/*.ts â services/*.ts â defService()
(withSchema/withRoute) (compose) (compose many) (router)
âââââââââââââââ
â Service â identitiesService(dataStores, configuration)
âââââââââââââââ¤
â Features ⡠getIdentityFeature
â ⡠findIdentitiesFeature
â ⡠updateIdentityFeature
â ⡠...
âââââââââââââââ
Why use services:
- One factory per domain â mount a complete REST (and optional WS) surface in one line
- Dependencies injected via
partialâ no global state - Composable â reuse features across services or build custom factories with the same pattern
- Typed
dataStoresandconfigurationper service (MongoDBCollectioninterfaces)
Import services via the single namespace export:
import { services } from '@nodeblocks/backend-sdk';
const { identitiesService, authService, chatService } = services;
See Feature Âģ for how features are defined and composed.
âī¸ How It Works (defService)â
defService turns a composed Composable into a running Express router. Every shipped service factory follows the same internal wiring:
defService(
partial(compose(...features), [{ dataStores, configuration, authenticate, ...drivers }]),
webSocketServer? // only chatService passes this today
);
When a request arrives, defService:
- Runs the composed adapter to build a
ServiceDefinition(routes, schemas, middleware, and injected dependencies) - Creates an Express router with
express.json()body parsing - Applies optional
service.middlewarefrom the composed definition - Registers HTTP routes (
GET/POST/PUT/PATCH/DELETE) â validators run before handlers - Binds WebSocket routes to the provided
webSocketServerwhen present
WebSocket routes assert if no webSocketServer is passed to defService.
chatService forwards the webSocketServer property of its optional third
argument as that second argument. It is not an export of the drivers
namespace.
Errors thrown during validation or handling are normalized to NodeblocksError and passed to Express via next. defService does not send the final error response; mount nodeBlocksErrorMiddleware() at the app level.
See Route Âģ for response handling ({ statusCode, data } terminators) and the WebSocket Service Guide Âģ for chat streaming setup.
đ§âđģ Standard Factory Patternâ
All SDK services share the same structure:
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';
identity?: { typeIds?: { admin: string; guest: string; regular: string } };
}
) => {
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 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, authenticate.
Optional drivers (accepted by the factory third argument and forwarded into the service context): mailService, fileStorageDriver, OAuth drivers (googleOAuthDriver, twitterOAuthDriver, lineOAuthDriver), and findAddressDriver. webSocketServer is handled separately: chatService passes it as the second argument to defService rather than injecting it into the request context.
When using cookie authentication (authMode: 'cookie'), register the cookie-parser Express middleware before mounting the service router.
đ SDK Service Catalogâ
Services are organized by domain in the SDK services namespace, matching src/services/index.ts:
| Service | Export | Responsibility | Drivers (3rd arg) | Docs |
|---|---|---|---|---|
| Authentication | authService | Registration, login/logout, MFA, tokens, invitations, OAuth | mailService, OAuth drivers | Docs Âģ |
| Identity | identitiesService | Identity CRUD, lock/unlock | â | Docs Âģ |
| Profile | profileService | Profiles, avatars, follows, likes | fileStorageDriver | Docs Âģ |
| Organization | organizationService | Organizations, members, change requests | fileStorageDriver | Docs Âģ |
| Product | productService | Product CRUD, batch/copy | fileStorageDriver | Docs Âģ |
| Attributes | attributesService | Attributes and groups | â | Docs Âģ |
| Category | categoryService | Category CRUD and status | â | Docs Âģ |
| Location | locationService | Hierarchical locations | â | Docs Âģ |
| Order | orderService | Order management | â | Docs Âģ |
| Chat | chatService | Channels, messages, templates, attachments, WS streaming | fileStorageDriver, webSocketServer | Docs Âģ |
| Notification | notificationService | List notifications, mark read (single/batch) | â | Docs Âģ |
| Address | addressService | Address lookup | findAddressDriver | Docs Âģ |
âšī¸ Import services from
@nodeblocks/backend-sdkvia theservicesnamespace:import { services } from '@nodeblocks/backend-sdk'.
Configuration notes:
authServiceacceptsPartial<AuthenticationServiceConfiguration>â defaults are merged internally viagetMergedAuthConfig(). See the Authentication Service doc for the full config surface.dataStorestypes mark some MongoDB collections as optional (e.g.invitations,onetimetokensfor auth;productVariantsfor products;organizationChangeRequestsfor organizations). See each per-service doc for required collections.
đ§âđģ Mounting a Serviceâ
All services are factory functions with this signature:
(dataStores, configuration, drivers?) => express.Router
The optional third drivers argument varies by service â see the catalog table above.
Basic HTTP serviceâ
import express from 'express';
import { middlewares, services, drivers } from '@nodeblocks/backend-sdk';
const { nodeBlocksErrorMiddleware } = middlewares;
const { identitiesService } = services;
const { withMongo } = drivers;
const connectToDatabase = withMongo(
'mongodb://localhost:27017',
'dev',
'user',
'password'
);
express()
.use(
'/api/identities',
identitiesService(
{
identities: await connectToDatabase('identities'),
},
{
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
authMode: 'bearer', // or 'cookie' â requires cookie-parser middleware
identity: {
typeIds: {
admin: 'admin-type-id',
guest: 'guest-type-id',
regular: 'regular-type-id',
},
},
}
)
)
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));
Services with driversâ
Some services require external drivers passed as the third argument:
import { services } from '@nodeblocks/backend-sdk';
const { authService, chatService, profileService } = services;
app.use('/api/auth', authService(dataStores, config, {
mailService,
googleOAuthDriver,
twitterOAuthDriver,
lineOAuthDriver,
}));
app.use('/api/profiles', profileService(dataStores, config, {
fileStorageDriver,
}));
app.use('/api/chat', chatService(dataStores, config, {
fileStorageDriver,
webSocketServer, // required for streamChatMessagesFeature
}));
đ§ Building a Custom Serviceâ
Compose any subset of features into your own factory â the SDK shipped services use the same pattern:
import { partial } from 'ramda';
import { features, primitives, utils } from '@nodeblocks/backend-sdk';
const { getIdentityFeature, findIdentitiesFeature } = features;
const { compose, defService } = primitives;
const { getBearerTokenInfo } = utils;
export const myIdentityService = (dataStores, configuration) =>
defService(
partial(
compose(getIdentityFeature, findIdentitiesFeature),
[{ dataStores, configuration, authenticate: getBearerTokenInfo }]
)
);
Pick features from the features namespace, inject only the drivers your selected routes need, and mount the returned router on Express. See Feature Âģ for feature composition details.
đ Good Practicesâ
- One domain per service factory â each shipped service targets one business area.
- Compose features, don't register routes manually â let
defServicewire validators and handlers from composed features. - Inject auth and drivers via
partialâ avoid globals; passauthenticate,mailService,fileStorageDriver, etc. in the deps object. - Mount error middleware at app level â
nodeBlocksErrorMiddleware()is the host app's responsibility, not part of the service factory. - Check driver requirements â file upload routes need
fileStorageDriver; OAuth and email flows need their respective drivers; chat WebSocket streaming needswebSocketServer.
âĄī¸ Nextâ
Start with the Authentication Service, or explore other services from the catalog above. For component-level details, see Feature Âģ, Route Âģ, Schema Âģ, and Blocks Âģ. For real-time chat, see the WebSocket Service Guide Âģ.