common-lib
Shared building blocks for the Zoop backend services: errors and the Express error handler, response helpers, request validation, service-to-service auth tokens, HTTP helpers, the Objection and Knex instances, and an abuse filter for chat, names and usernames.
- npm: @fluxlabs/common-lib
- Release notes: GitHub releases
npm install @fluxlabs/common-lib
Usage
Errors and responses
Throw a CustomError subclass and let errorHandler, registered after every route, turn it into a response:
import express from "express";
import { BadRequestError, errorHandler, okResponse } from "@fluxlabs/common-lib";
const app = express();
app.get("/stores/:id", async (req, res, next) => {
try {
const store = await Store.query().findById(req.params.id);
if (!store) throw new BadRequestError("No such store");
return okResponse(res, store, "Store fetched");
} catch (err) {
next(err);
}
});
app.use(errorHandler);
| Call | Status | Body |
|---|---|---|
okResponse(res, data, message) | 200 | { success: true, statusCode: 200, data, message } |
createdResponse(res, data, message) | 201 | { success: true, statusCode: 201, data, message } |
noContentResponse(res, message) | 204 | none |
errorResponse(res, data, message, code), unAuthorisedResponse(...) | code | { success: false, code, data, message } |
errorHandler, for a CustomError | its statusCode | { errors: [{ message, field? }] } |
errorHandler, for anything else | 400 | { errors: [{ message: "Something went wrong" }] } |
The error classes, and the factories that build a ConcreteCustomError with any status:
| Error | Status |
|---|---|
new BadRequestError(message) | 400 |
new NotAuthorizedError() | 401 |
new NotFoundError() | 404 |
new RequestValidationError(errors) | 400, one entry per invalid field |
new DatabaseConnectionError() | 500 |
createStatusCodeError(statusCode, message, body?) | statusCode |
badRequestError(message) | 422, unlike BadRequestError |
unauthorizedError(message), forbiddenError(message), notFoundError(message) | 401, 403, 404 |
unverifiedError(message, body) | 412 |
Request validation and auth
import { body } from "express-validator";
import { currentUser, requireAuth, validateRequest } from "@fluxlabs/common-lib";
app.use(currentUser); // req.currentUser, from the JWT in req.session.jwt, verified with JWT_KEY
app.post("/orders", requireAuth, [body("store_id").isUUID()], validateRequest, createOrder);
requireAuth throws NotAuthorizedError when there is no req.currentUser, and validateRequest throws RequestValidationError when an express-validator check failed.
Service-to-service calls
Internal endpoints take a short-lived JWT, scoped to one service's internal API, in the x-internal-token header. The service that owns the endpoints verifies it:
import { createInternalServiceAuth, internalAudience } from "@fluxlabs/common-lib";
export const verifyInternalToken = createInternalServiceAuth({
audience: internalAudience("order-service"),
getSecret: () => environment_config.ORDER_INTERNAL_JWT_SECRET,
});
router.get("/internal/store-detail/:id", verifyInternalToken, handler); // sets req.internal_service
The service that calls them mints one per request:
import { axios, internalAudience, internalServiceAuthHeaders } from "@fluxlabs/common-lib";
await axios.get(url, {
headers: internalServiceAuthHeaders({
issuer: "payment-service",
audience: internalAudience("order-service"),
secret: environment_config.ORDER_INTERNAL_JWT_SECRET,
}),
});
Tokens live 30 seconds. getSecret is a function so that a secret loaded after startup, or rotated, is picked up per request. src/utils/internal-service-token.ts has the details.
HTTP
handleData (POST), handlePatchData (PATCH) and getData (GET) never throw. They resolve to { result, error }, where result is the axios response and error what was thrown:
import { handleData } from "@fluxlabs/common-lib";
const { result, error } = await handleData(url, payload, { headers }, "json"); // "form" sends it form-encoded
if (error) { /* ... */ }
axios is the default axios instance.
Database and Supabase
Model is Objection's Model and Knex the knex factory, so that every service uses the same instances. initializeSupabase(url, key) returns a SupabaseClient.
Abuse filter
Checks text for abuse in English, Hindi and Hinglish, in Latin script or Devanagari. Each call returns where the abuse was found, "latin", "devanagari" or "emoji", or null when the text is clean:
import { findAbuse, findAbuseInName, findAbuseInUsername } from "@fluxlabs/common-lib";
findAbuse("tu chutiya hai"); // "latin" — for sentences, such as chat comments
findAbuseInName("iamchutiya99"); // "latin" — for full names
findAbuseInName("harshit.dikshit"); // null
findAbuseInUsername("jiya_bkl"); // "latin" — for usernames
| Call | Use it for | Catches |
|---|---|---|
findAbuse(text) | sentences | abuse and insults, mild ones included, however they are disguised: case, stretched letters (fuuuck), leetspeak (chut1ya), look-alike characters, letters spelled out with separators (c.h.u.t.i.y.a), masked letters (f**k), long words split by spaces (chu tiya), Devanagari phrases, and abusive emoji |
findAbuseInName(text) | full names | severe abuse only, so that no real name is refused, but also when it is joined to other letters (bigchutiya) or split by _, . or - (big_chutiya) |
findAbuseInUsername(text) | usernames | everything findAbuseInName catches, plus short forms (bkl, mkc, bsdk, bc) when they stand as a word of their own (jiya_bkl, mkc99, b.k.l). Not inside a word, since ramkc (Ram KC) holds mkc, and not in full names, where they could be initials (BC Roy) |
All three run in memory, in well under a millisecond, and return null when the check itself fails, so a broken filter never blocks a comment or a signup. The matchers are built on the first call, so a service that never checks text never pays for them.
The word lists are in src/services/moderation-service/abuse-words.ts, one set for each call. Chat's are only the defaults: setAbuseLists replaces them while the service runs, and notification-service does that from the Firestore document chat_moderation/abuse_words, so chat words change without a release.
import { DEFAULT_ABUSE_LISTS, setAbuseLists } from "@fluxlabs/common-lib";
setAbuseLists({ ...DEFAULT_ABUSE_LISTS, hinglish: [...DEFAULT_ABUSE_LISTS.hinglish, "|newword"] }); // true
A list that cannot be built is refused, setAbuseLists returns false, and the lists in use stay. findAbuseInName's and findAbuseInUsername's lists are fixed, because chat's include words that are also names ("Kamini"). It allows real names that a pattern matches, such as the surnames Chutia and Bhadwal; when a real name is blocked, add it to NAME_ALLOWLIST.
Types
- Importing the package adds
currentUser?: UserPayloadandinternal_service?: stringto Express'sRequest. Model,Knex,axios, the HTTP helpers'result, and the response helpers'resand return value are typedanyon purpose. Typing them properly breaks code in the services that compiles today only because they areany, so it needs a major version. The comments insrc/instances/index.tsandsrc/utils/function.tsexplain.
Development
nvm use # Node 22, from .nvmrc
npm ci
npm run build # tsc, into build/
Every export has to declare its type in source: the build fails otherwise (isolatedDeclarations). That keeps the published .d.ts from changing with whatever dependencies the publishing machine has installed, which is how 1.0.14 broke four services' builds.
A change to a public type can break the services that use this package even when this package builds, so check them before releasing one.
A change reaches the services only in a new version: see Releasing.
Releasing
Bump the version, or nothing is released. A merge to
mainpublishes only when it leavespackage.jsonwith a version npm doesn't have yet. So a pull request whose change should reach the services has to bump the version itself, or be followed by one that does. That includes a change to this README, since npm shows the README of the latest published version.
- In the pull request, bump the version:
npm version patch --no-git-tag-versionupdatespackage.jsonandpackage-lock.json. Usepatchfor a fix,minorfor an addition such as a new export, andmajorfor anything that can break a service's build, such as narrowing a type. - Write the pull request's description as the release notes: everything above its
## Test planheading becomes them. - Merge it. The Release workflow builds the package, publishes it to npm through trusted publishing, with no npm token, and creates the
v<version>GitHub release with those notes and a link to the version on npm.
A prerelease version, such as 1.1.0-beta.0, is published under the next tag instead of latest. Publish only through this workflow, never with npm publish or npm run pub from your own machine.