controller-sets v3.3.0

Every export#

Everything is a named export from the package root. The package is ESM-only.

import { createRouter, errorHandler, ValidationError } from 'express-controller-sets';
ExportKindPurpose
CRUD
createRouterfunctionA router with the six CRUD routes.
ControllerSetsclassThe six handlers, for routes you wire yourself.
isQueryMethodSupportedfunctionWhether this runtime can serve HTTP QUERY.
createRouterS3uploadfunctionDeprecated — use createRouter({ upload }). Upgrading
Errors
errorHandlermiddlewareTurns thrown errors into the JSON envelope. Mount last.
HttpErrorclassAn error with a status and a client-safe message.
ValidationErrorclassA 400 with per-field messages.
Uploads
fileUploadMiddlewarefunctionS3 upload middleware for any route.
compressImagefunctionCompress an image buffer. Needs sharp.
Cache
createRedisCacheStorefunctionA Redis store to share across routers.
createMemoryCacheStorefunctionAn in-process store for development and tests.
Authentication
createAuthRouterfunctionSign-up, login and user management. Guide
identifierfunctionA login identifier with a custom normalizer.
requireAuth · requireRolefunctionStandalone guards. Prefer auth.requireAuth.
buildAuthConfigfunctionResolve auth options without mounting a router.
hashPassword · verifyPasswordfunctionThe scrypt password hashing the router uses.
signToken · verifyTokenfunctionThe HS256 JWT functions the router uses.
AUTH_ROUTESconstantEvery auth endpoint with its default path and access level.
BUILT_IN_PROVIDERSconstantThe social providers available by name.
DEFAULT_TEMPLATESconstantThe built-in email and SMS templates. Templates
Utilities
escapeRegex(value)functionEscape regex metacharacters so input matches literally.
QUERY_FILTER_OPERATORSconstantThe QUERY operator table: { eq: '$eq', gt: '$gt', … }.

createRouter#

createRouter<T>(options: RouterOptions<T>): Router & { invalidateCache(): Promise<void> }

Mounts GET /, QUERY / (when supported and enableQuery isn't false), POST /, GET /:id, PATCH /:id and DELETE /:id. Throws at startup if model is missing or upload isn't true or an object. Options: createRouter.

ControllerSets#

new ControllerSets<T>(options: ControllerOptions<T>)

controller.getAll(req, res)      // GET /
controller.query(req, res)       // QUERY /
controller.get(req, res)         // GET /:id
controller.create(req, res)      // POST /
controller.update(req, res)      // PATCH /:id
controller.delete(req, res)      // DELETE /:id
controller.invalidateCache(): Promise<void>
controller.config                // resolved, frozen options

Takes every router option except middlewares, enableQuery and upload. Handlers are pre-bound. getById and queryAll are deprecated aliases of get and query. The positional constructor new ControllerSets(model, orderBy, query, search) is deprecated. Guide

isQueryMethodSupported#

isQueryMethodSupported(): boolean

true when Node's HTTP parser knows QUERY (22.2+) and Express's router exposes router.query(). Check it before calling router.query() yourself.

errorHandler#

app.use('/api/products', productRouter);
app.use(errorHandler);   // after every route
  • HttpError / ValidationError → their status and message (and fields).
  • Mongoose validation and cast errors → 400. Duplicate key → 409 naming the fields. Multer limits → 400.
  • Anything else → 500 "Internal Server Error". The detail is logged with a requestId, which is also sent to the client.
  • stack is added only when NODE_ENV=development.

HttpError and ValidationError#

new HttpError(status: number, message: string, options?: { headers?: Record<string, string> })
new ValidationError(message: string, fields?: Record<string, string>, status = 400)

Throw them from validate hooks, onLogin, or your own middleware. The message is sent to the client as-is, so write it for the client. ValidationError extends HttpError; its fields become the response's fields.

throw new HttpError(403, 'Only the author can publish.');
throw new ValidationError('Check the submitted values.', { email: 'is already registered' });

fileUploadMiddleware and compressImage#

fileUploadMiddleware(req, res, next, options?: UploadOptions): void
compressImage(buffer: Buffer, mimetypeOrFormat: string, level: 'low' | 'medium' | 'med' | 'high'): Promise<Buffer>

UploadOptions is the router's upload option with uploadPath in place of path. compressImage returns non-compressible formats unchanged. Guide

Cache stores#

createRedisCacheStore({ url?: string, client?: RedisClient, logger?: Logger }): CacheStore
createMemoryCacheStore({ maxEntries = 1000 }?): CacheStore & { readonly size: number }

interface CacheStore {
  get(key: string): Promise<string | null | undefined>;
  set(key: string, value: string, ttlSeconds: number): Promise<unknown>;
}

Pass a store as cache: { store }. createRedisCacheStore needs a url or a client (ioredis or node-redis). Any object implementing CacheStore works. Guide

createAuthRouter and identifier#

createAuthRouter<T>(options: AuthOptions<T>): AuthRouter

interface AuthRouter extends Router {
  requireAuth: RequestHandler;                          // sets req.auth = { userId, role, claims }
  requireRole(...roles: (string | string[])[]): RequestHandler;
  config: ResolvedAuthConfig;
  urls: readonly { name, method, path, access }[];      // what was mounted
}

identifier(field: string, normalize?: (value: string) => string): { field, normalize }

Options: Authentication → All options.

Auth building blocks#

For wiring authentication by hand. Most apps only need createAuthRouter and the guards on the router it returns.

buildAuthConfig(options: AuthOptions): ResolvedAuthConfig
requireAuth(config: ResolvedAuthConfig, { loadUser = false }?): RequestHandler   // loadUser also sets req.user
requireRole(...roles: (string | string[])[]): RequestHandler

hashPassword(plain: string): Promise<string>
verifyPassword(plain: string, stored: string): Promise<boolean>

signToken(claims: object, { secret, expiresIn, issuer?, audience? }): string
verifyToken(token: string, { secret, issuer?, audience?, clockToleranceSeconds = 5 }): object   // throws HttpError(401)

TypeScript#

Declarations ship with the package. Generic options take your document type:

import { createRouter, type RouterOptions } from 'express-controller-sets';
import Product, { type IProduct } from './models/Product.js';

const options: RouterOptions<IProduct> = {
  model: Product,
  orderBy: '-createdAt',
  query: ['category'],
  allowedFields: ['name', 'price'],
  onGet: async () => ({ populates: 'category', selects: 'name price' }),
};

app.use('/api/products', createRouter(options));

Type the QUERY body on the client to catch mistakes before sending:

import type { QueryRequestBody } from 'express-controller-sets';

const body: QueryRequestBody = { filter: { price: { gte: 50 }, tag: ['sale'] }, sort: ['-price', 'name'] };
AreaTypes
Router & controllerRouterOptions, ControllerOptions, ControllerRouter, FieldPolicy, OnGetFn, OnGetResult, Logger
WritingValidateFn, ValidateContext, ValidatePolicy, WriteOperation
ReadingPagination, PaginationMode, CountStrategy, QueryRequestBody, FilterCondition, FilterOperators, FilterScalar
UploadsUploadOptions, RouterUploadOptions, UploadField, ImageOptimizationLevel
CacheCacheOptions, CacheStore
AuthAuthOptions, AuthRouter, AuthContext, TokenOptions, RefreshOptions, RoleOptions, OtpOptions, MailOptions, SmsOptions, MailTemplate, SmsTemplate, TemplateContext, SocialProvider, SocialProfile, AuthRouteName, AuthUrl, AuthFieldMap, IdentifierSpec

Importing the package also types req.auth on Express's Request.

Field names aren't type-checked. allowedFields is string[], so 'pirce' compiles and silently drops the real field. If a field won't save, check the spelling first.

Esc
↑ ↓ to move↵ to open