@shoutem/express-stack
v2.0.0
Published
Shoutem express stack
Maintainers
Readme
@shoutem/express-stack
Shared building blocks for Shoutem backend services running on Express + Mongoose. Bundles repositories, controllers, middleware, ACL helpers, structured logging, Sentry integration, distributed locking, and a pluggable cache layer.
Install
npm install @shoutem/express-stackPeer dependency: mongoose >= 5.11.10.
Modules
All public exports live in src/index.js.
| Area | Exports |
|---|---|
| ACL | assertCanCreate, assertCanRead, assertCanUpdate, assertCanRemove, assertCanAccess, assertCanPerformAction, assertCanPerformActionIo, addAclFilterToQuery, getSecurityContext |
| Controllers | CrudController, CrudSubDocumentController |
| Repositories | CrudMongoRepository, CrudMongoSubDocumentRepository |
| Middleware | asyncMiddleware, asyncParamMiddleware, loadDocument, loadSubDocument, injectLocalIntoIo, favicon |
| Locals | getLocals, setLocals |
| Cache | cacheDecorator, cacheDecoratorLegacy, invalidateCacheDecorator, invalidateCacheDecoratorLegacy, cacheProvider, memoryCacheProvider, redisCacheProvider |
| Mutex | lockHandler, mutexErrorHandler, lockProvider |
| Logging | logger, createLogger |
| Sentry | captureException, sentryMiddleware |
| Errors | setErrorServiceName, generateErrorCode |
| Paging | PagedCollection |
| Dependency | buildSchemaDependencyGraph, createNodeTopology |
| Request utils | requestUtils (buildFilterQuery, buildPageQuery) |
| Env | requireEnvString, requireEnvNumber, requireEnvBoolean |
| Clients | redisClient, redisLockClient |
Usage
CRUD pipeline
import {
CrudMongoRepository,
CrudController,
loadDocument,
asyncMiddleware,
} from '@shoutem/express-stack';
const repository = new CrudMongoRepository(UserModel);
const controller = new CrudController(repository, { resource: 'user' });
router.param('userId', loadDocument(repository, { resource: 'user' }));
router.get('/users/:userId', asyncMiddleware(controller.get));
router.get('/users', asyncMiddleware(controller.search));
router.post('/users', asyncMiddleware(controller.create));
router.patch('/users/:userId', asyncMiddleware(controller.update));
router.delete('/users/:userId', asyncMiddleware(controller.remove));Cache decorators
cacheProvider auto-selects Redis when REDIS_CONNECTION_STRING is set, otherwise falls back to in-memory.
import { cacheDecorator, cacheProvider } from '@shoutem/express-stack';
class UserRepository {
@cacheDecorator(
(id) => `user:${id}`,
(id) => [`user-invalidation:${id}`],
cacheProvider,
300, // ttl seconds
)
async getById(id) {
return UserModel.findById(id).lean();
}
}Use invalidateCacheDecorator on writes to flush the dependency keys; use the *Legacy variants for callback-based methods.
Distributed locking
import { lockHandler, mutexErrorHandler } from '@shoutem/express-stack';
router.post(
'/applications/:appId/sync',
lockHandler('app-sync'),
asyncMiddleware(syncController.run),
mutexErrorHandler,
);When USE_REDIS_LOCK=true and Redis is reachable, locks are acquired via Redlock; otherwise both lock and unlock are no-ops, so local development "just works."
Logging
import { createLogger, logger } from '@shoutem/express-stack';
// default singleton — configured via env vars
logger.info('starting');
// or build a custom one
const log = createLogger({ logService: 'sync-worker', logLevel: 'debug' });Sentry
import { sentryMiddleware, captureException } from '@shoutem/express-stack';
app.use(sentryMiddleware({
errorTypeBlacklist: [MyKnownClientError],
}));
try {
await doRiskyThing();
} catch (err) {
captureException(err);
throw err;
}Errors and locals
import { setErrorServiceName, generateErrorCode } from '@shoutem/express-stack';
setErrorServiceName('payment-manager');
const code = generateErrorCode('stripe', 'validation', 'invalidCard');
// => 'payment-manager_stripe_validation_invalidCard'import { getLocals, setLocals } from '@shoutem/express-stack';
setLocals(req, 'auth', { userId, securityContext });
const userId = getLocals(req, 'auth.userId');Env helpers
import {
requireEnvString,
requireEnvNumber,
requireEnvBoolean,
} from '@shoutem/express-stack';
const port = requireEnvNumber('PORT', 3000);
const host = requireEnvString('HOST', '0.0.0.0');
const debug = requireEnvBoolean('DEBUG', false);Throws if the variable is missing and no default is provided.
Configuration (env vars)
| Variable | Default | Used by |
|---|---|---|
| LOG_LEVEL | info | logging |
| LOG_SERVICE | logger | logging |
| LOG_DIR | (unset — file transport disabled) | logging |
| LOG_STDOUT | true | logging |
| REDIS_CONNECTION_STRING | '' | redis client |
| REDIS_USE_TLS | true | redis client |
| USE_REDIS_LOCK | false | redlock client |
| MUTEX_RETRY_COUNT | 4 | redlock client |
| MUTEX_RETRY_DELAY | 4000 | redlock client |
| MUTEX_DURATION_TIME | 15000 | mutex |
| CACHE_TIME_TO_LIVE | 900 (seconds) | cache |
| SENTRY_DSN | (unset — Sentry disabled) | sentry |
| SENTRY_ENVIRONMENT | (unset) | sentry |
| SENTRY_SERVICE_NAME | (unset) | sentry |
When REDIS_CONNECTION_STRING is empty, cacheProvider and redisLockClient degrade to memory cache and no-op lock respectively.
Cache provider contract
Both MemoryCacheProvider and RedisCacheProvider expose:
get(key) -> Promise<value | null | undefined>set(key, value, ttl?) -> Promise<void>del(key) -> Promise<void>
cache-decorators rely on this surface. If you implement a custom provider, match these signatures — there's a contract test in src/cache/tests/cache-provider-contract.spec.js.
Development
npm install
npm test # mocha + babel
npm run lint # eslint --fix
npm run build # babel ./src -> ./build
npm run release # publish @latest
npm run release-rc # publish @rcTests use mongodb-memory-server for the Mongoose specs — no local Mongo required.
Repository layout
src/
acl/ # role-based assertions, security context, query filtering
cache/ # decorators + memory/redis providers
controller/ # CRUD controllers
dependency/ # schema dependency graph + topological sort
env/ # typed env-var helpers
error/ # error code generator
locals/ # request-scoped storage
logging/ # winston-based logger
middleware/ # async wrappers, document loaders, favicon
mutex/ # lock middleware + error handler + provider
paging/ # PagedCollection
redis/ # ioredis client singleton
redis-lock/ # redlock client singleton
repository/ # Mongoose CRUD + JSON:API → Mongo query parser
request-utils/ # JSON:API query string builders
sentry-io/ # Raven middleware + captureException