zoopse-crm-shared
v1.2.2
Published
Shared utilities for the Zoopse CRM backend. This is a small, dependency-light **internal library** (npm package `zoopse-crm-shared`) consumed by every microservice — the API Gateway, Auth, Sales, Subscription, and Notification services — to keep cross-cu
Readme
zoopse-crm-shared
Shared utilities for the Zoopse CRM backend. This is a small, dependency-light internal library (npm package zoopse-crm-shared) consumed by every microservice — the API Gateway, Auth, Sales, Subscription, and Notification services — to keep cross-cutting concerns (errors, responses, gateway auth, logging, Kafka, helpers) consistent across the platform.
It is not a service: there is no server, database, or routes here — just ES module exports.
Table of contents
- Installation & usage
- What's exported
- Errors
- Common response
- Gateway middleware
- Logger
- Kafka publisher
- Helpers
- How services use it
- Versioning & publishing
Installation & usage
The package is ESM ("type": "module"). Each service depends on it ("zoopse-crm-shared": "^1.2.x") and imports named exports from the package root:
import {
CustomError, BadRequestError, NotFoundError, NotAuthorizedError, FileTooLargeError, ServerError,
verifyGatewayRequest,
sendCommonResponse,
winstonLogger,
KafkaPublisher,
firstLetterUppercase, lowerCase, toUpperCase, isEmail, isDataURL
} from 'zoopse-crm-shared';Runtime dependencies: http-status-codes, jsonwebtoken, kafkajs, winston, winston-elasticsearch.
Several services install this from a local path / workspace and run a predev check (npm ls zoopse-crm-shared) to ensure it resolves before booting.
What's exported
| Export | Module | Kind | Purpose |
|--------|--------|------|---------|
| CustomError | src/errors.js | class | Base error with comingFrom + serializeErrors() |
| BadRequestError | src/errors.js | class | 400 |
| NotFoundError | src/errors.js | class | 404 |
| NotAuthorizedError | src/errors.js | class | 401 |
| FileTooLargeError | src/errors.js | class | 413 |
| ServerError | src/errors.js | class | 503 |
| verifyGatewayRequest | src/gateway-middleware.js | middleware factory | Validates the gatewaytoken header |
| sendCommonResponse | src/commonResponse.js | function | Standard success-response envelope |
| winstonLogger | src/logger.js | factory | Console + optional Elasticsearch logger |
| KafkaPublisher | src/kafka.publisher.js | class | Lazy-connecting Kafka producer |
| firstLetterUppercase, lowerCase, toUpperCase, isEmail, isDataURL | src/helper.js | functions | String/validation helpers |
Errors
A small hierarchy of typed HTTP errors. All extend CustomError, which captures a comingFrom source label and exposes serializeErrors() — the shape every service's error handler returns for a CustomError.
export class CustomError extends Error {
constructor(message, comingFrom) { super(message); this.comingFrom = comingFrom; }
serializeErrors() {
return { message: this.message, statusCode: this.statusCode, status: this.status, comingFrom: this.comingFrom };
}
}| Class | statusCode | status |
|-------|--------------|----------|
| BadRequestError | 400 | error |
| NotAuthorizedError | 401 | error |
| NotFoundError | 404 | error |
| FileTooLargeError | 413 (REQUEST_TOO_LONG) | error |
| ServerError | 503 (SERVICE_UNAVAILABLE) | error |
Usage — throw with a message + a source label; the centralized error handler serializes it:
import { BadRequestError } from 'zoopse-crm-shared';
if (!email) throw new BadRequestError('email is required', 'SignUp Ctrl');Each service's errorHandler checks err instanceof CustomError and returns err.serializeErrors() with err.statusCode. The gateway additionally maps upstream HTTP statuses back to these classes (400→BadRequestError, 401→NotAuthorizedError, 404→NotFoundError, 413→FileTooLargeError, else ServerError).
Note: a bare
new CustomError(message, comingFrom)has nostatusCode/status; handlers fall back to a default (usually 400 or 500). Prefer a concrete subclass.
Common response
sendCommonResponse(res, statusCode, message, data?, pagination?)Writes the platform's standard success envelope and returns the Express response:
{ "success": true, "message": "...", "data": { }, "pagination": { } }datais included only when notundefined.paginationis included only when truthy (services pass{ total, page, limit }or{ total, skip, limit, hasMore }).
import { sendCommonResponse } from 'zoopse-crm-shared';
sendCommonResponse(res, 200, 'leads fetched', leads, { total, page, limit });Gateway middleware
verifyGatewayRequest(gatewaySecret = process.env.GATEWAY_JWT_SECRET)An Express middleware factory that protects downstream services so they only accept traffic from the API Gateway. It:
- Requires a
gatewaytokenrequest header (elseNotAuthorizedError). - Verifies the token's JWT signature against
gatewaySecret. - Requires the decoded
payload.idto be one of the known service identifiers:auth,sales,notification,subscription. - Calls
next()on success; otherwise throwsNotAuthorizedError('Request coming from invalid source').
The gateway signs this token per outbound service (jwt.sign({ id: '<service>' }, GATEWAY_JWT_TOKEN)), so GATEWAY_JWT_TOKEN on the gateway must equal the gatewaySecret passed here by each service.
import { verifyGatewayRequest } from 'zoopse-crm-shared';
app.use('/api/v1/auth', verifyGatewayRequest(config.GATEWAY_JWT_SECRET), authRoutes());The same secret is also used for service-to-service gRPC
gatewaytokenmetadata (e.g. Subscription → AuthGetTenantInvoiceProfile).
Logger
winstonLogger(elasticsearchNode, name, level)Creates a Winston logger with a colorized console transport and an optional Elasticsearch transport. Elasticsearch is enabled only when:
elasticsearchNode(URL) is provided, andNODE_ENV !== 'test', andDISABLE_ELASTICSEARCH_LOGGER !== '1'.
defaultMeta.service is set to name, and exitOnError is false. winston-elasticsearch is loaded lazily (via createRequire) only when ES logging is active, so test/dev runs don't pull it in.
import { winstonLogger } from 'zoopse-crm-shared';
const log = winstonLogger(process.env.ELASTIC_SEARCH_URL, 'auth-service', 'debug');Most services wrap this in a local getLogger() that falls back to a plain console logger when ES is unconfigured.
Kafka publisher
new KafkaPublisher(topic, { KAFKA_CLIENT_ID, KAFKA_BROKERS })
await publisher.publish(payload) // payload must include a `key`A thin KafkaJS producer wrapper:
- Requires a
topic(throws otherwise). - Parses
KAFKA_BROKERS(comma-separated; defaults tolocalhost:9092) and usesKAFKA_CLIENT_ID(defaultdefault-client). - Lazily connects on first
publishand reuses the connection. - Sends one message per call:
{ key: payload.key, value: JSON.stringify(payload) }.
import { KafkaPublisher } from 'zoopse-crm-shared';
const publisher = new KafkaPublisher('notification', {
KAFKA_CLIENT_ID: config.KAFKA_CLIENT_ID,
KAFKA_BROKERS: config.KAFKA_BROKERS
});
await publisher.publish({ key: 'create-notification', data: { /* ... */ } });Consumers read message.value and JSON.parse it — so the full payload (including key) is serialized into the value. Used by: the gateway (lead webhooks, bulk upload), Sales (overdue-task notifications), and Subscription (webhook fan-out).
The wrapper logs via
console, not the Winston logger, and has no built-in retry/backoff beyond KafkaJS defaults.
Helpers
src/helper.js — small pure functions:
| Function | Behaviour |
|----------|-----------|
| firstLetterUppercase(str) | Title-cases each space-separated word (lowercases the rest). |
| lowerCase(str) | str.toLowerCase(). |
| toUpperCase(str) | Upper-cases, but returns the input unchanged when falsy. |
| isEmail(email) | Regex email validation. |
| isDataURL(value) | True if the string is a data: URL. |
import { isEmail } from 'zoopse-crm-shared';
existingUser = isEmail(username)
? await getUserByEmail(username)
: await getUserByUsername(username);How services use it
| Export | Primary consumers |
|--------|-------------------|
| Error classes + CustomError | every service (thrown in controllers/services; serialized in each error handler) |
| sendCommonResponse | every service's controllers |
| verifyGatewayRequest | Auth, Sales, Subscription (route guards); Auth gRPC also reuses the secret |
| winstonLogger | every service (usually behind a local getLogger) |
| KafkaPublisher | Gateway, Sales, Subscription |
| Helpers | Auth (isEmail), Sales, others as needed |
This single library is what keeps response shapes, error contracts, the gateway trust boundary, log formatting, and Kafka messaging identical across services — change it here and every service inherits it on the next version bump.
Versioning & publishing
- Package:
zoopse-crm-shared, current version 1.2.1, license ISC,"type": "module", entryindex.js. - Tests:
npm test(node --test) — seetests/. - All public API is re-exported from
index.js; add new exports there. - Consumers pin
^1.2.x; bump the version when changing any exported contract (error shapes, response envelope, the gateway token's allowedids, etc.) since all five services depend on it.
