@wetech-code_official/common-lib
v1.10.0
Published
Reusable common utilities library
Maintainers
Readme
common-lib
Reusable TypeScript utilities and data clients for Node.js services: Redis, Postgres, Mongoose, Express request/response helpers, an internal-module proxy, and a shared Result tuple convention.
Install From npm
npm install @wetech-code_official/common-libThis package is published to the public npm registry under the @wetech-code_official scope.
Install From GitHub Packages
npm install @wetech-code/common-lib --registry=https://npm.pkg.github.comThe GitHub Packages copy is published under the @wetech-code organization scope — same code, different package name. When consuming from GitHub Packages, use @wetech-code/common-lib in place of @wetech-code_official/common-lib in every example below.
Package Structure
src/
index.ts re-exports a curated subset of every subpath below
api/ ApiRequest/ApiResponse envelope types + helpers
config/ env-driven JSON config loader
errors/ AppError
express/ Validator, AppRoute/registerRoutes, sendSuccess/sendApiError, middleware
http/ InternalApiHttpClient base class
mongoose/ MongooseOps
postgres/ PostgresPoolOps
proxy/ ModuleProxyService
redis/ RedisOps
result/ okResult/failResult/unwrapResult
sql/ DatabaseNames/DefaultTimeout enums
types/ the Result tuple type
internal/ implementation-only, never published
examples/
dist/ only this, README.md, and LICENSE are publishedEach directory under src/ (except internal/) is also its own subpath export, e.g. @wetech-code_official/common-lib/redis. The package root only re-exports a curated subset — some symbols (e.g. HttpMethod/HttpStatusCode as runtime values, sendApiError, sendApiSuccess, getApiRequest, sendFileDownload) are only available via their subpath. Prefer importing from the specific subpath you need.
import { RedisOps, PostgresPoolOps, MongooseOps } from "@wetech-code_official/common-lib";
// or, for the full surface of a module:
import { RedisOps } from "@wetech-code_official/common-lib/redis";
import { PostgresPoolOps } from "@wetech-code_official/common-lib/postgres";
import { MongooseOps } from "@wetech-code_official/common-lib/mongoose";
import type { Result } from "@wetech-code_official/common-lib/types";Result Convention
Most data-layer methods (Redis, Postgres) return a Result tuple instead of throwing: [data | null, Error | null].
import { failResult, okResult, unwrapResult } from "@wetech-code_official/common-lib/result";
import type { Result } from "@wetech-code_official/common-lib/types";
async function findUser(id: string): Promise<Result> {
const user = await db.users.findById(id);
return user ? okResult(user) : failResult(new Error("lookup failed"));
}
// Unwrap into typed data, or throw an AppError(404) if data is null/error is set.
const user = unwrapResult<User>(await findUser(id), {
message: "User not found",
code: "USER_NOT_FOUND",
});Redis — RedisOps
import { RedisOps } from "@wetech-code_official/common-lib/redis";
const redis = await RedisOps.connect({
type: "socket", // or { type: "url", url: "redis://..." }
host: "127.0.0.1",
port: 6379,
username: "default",
password: "...",
retryMaxDelayMs: 30_000, // reconnect backoff cap
retryMaxRetries: 3,
retryBaseDelayMs: 200,
});
await redis.set("session:123", "token", 60); // 60s TTL
const [value] = await redis.get("session:123");
await redis.disconnect();Connects lazily on first call, reconnects automatically on socket errors with exponential backoff, and registers SIGINT/SIGTERM/uncaughtException handlers after the first successful connect.
Every method returns a Result and takes an optional trailing tnxKey to run queued inside a transaction:
| Category | Methods |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Strings | set, get, del, mSet, mGet, mDel, setNx, setEx, getEx, delEx, incr, decr, incrBy, decrBy, exists, rename |
| Hashes | hSet, hGet, hDel, hExists, hGetAll, hIncrBy, hDecrBy |
| Lists | lPush, rPush, lPop, rPop, lRange, lLen, lIndex, lSet, lRem |
| Sets | sAdd, sRem, sIsMember, sMembers, sCard, sPop, sRandMember, sUnion, sInter, sDiff |
| Sorted sets | zAdd, zRem, zScore, zRange, zCard, zRank, zRevRank, zIncrBy, zDecrBy |
| Pub/sub | publish, subscribe, unsubscribe, pSubscribe, pUnsubscribe (payloads are JSON-encoded objects) |
| Misc | flushAll |
Transactions are Redis MULTI/EXEC: multi/exec/discard/watch/unwatch directly, the mongoose-style aliases startTransaction/commitTransaction/abortTransaction, or the wrapper:
await redis.withTransaction(async (tnxKey) => {
await redis.set("a", "1", undefined, tnxKey);
await redis.set("b", "2", undefined, tnxKey);
});MULTI is not a rollback transaction — DISCARD only cancels still-queued commands, it can't undo an already-executed EXEC. Use watch for optimistic concurrency if you need that guarantee.
Postgres — PostgresPoolOps
import { PostgresPoolOps } from "@wetech-code_official/common-lib/postgres";
const pg = await PostgresPoolOps.connect({
host: "localhost",
port: 5432,
user: "postgres",
password: "...",
database: "mydb",
});
const [result, err] = await pg.query("select * from users where id = {id}", { id: userId });Queries use {name} placeholders, not $1 — query/multiQuery compile them to parameterized $1, $2, ... SQL internally, so values are always bound, never string-interpolated. Every unused param throws, and every placeholder must have a param — this catches typos early. The Result shape depends on the SQL command: INSERT/UPDATE/DELETE return a { message, success } summary, SELECT returns { rows, rowsCount, success, message }.
Auto-creates the target database on first connect if it doesn't exist (connects to the postgres database and issues CREATE DATABASE), and rebuilds the pool with backoff if it goes unhealthy (maxRetries/reconnectingDelay in PoolConfig, defaulting to DefaultTimeout.MaxRetries/DefaultTimeout.RECONNECTING_DELAY) — exits the process if reconnection is exhausted.
Transactions: startTransaction(timeoutMs?) returns a transactionKey in its Result data (auto-rolls-back if not committed within the timeout, default DefaultTimeout.TRANSACTION = 60s); pass that key as the last arg to query/multiQuery, then commitTransaction(key) / abortTransaction(key). Or use the wrapper:
await pg.withTransaction(async (key) => {
await pg.query("insert into orders (id) values ({id})", { id }, key);
});Mongoose — MongooseOps
import { Schema } from "mongoose";
import { MongooseOps } from "@wetech-code_official/common-lib/mongoose";
const mongo = await MongooseOps.connect("mongodb://127.0.0.1:27017/mydb");
await mongo.registerModel("User", new Schema({ name: String }));
const User = mongo.getModel<{ name: string }>("User");Registers models against a dedicated Connection (not the global mongoose singleton), so multiple MongooseOps instances don't collide. Transactions need a replica set or mongos — a standalone mongod throws:
await mongo.withTransaction(async (session) => {
await User.create([{ name: "Ada" }], { session });
});or the manual handle API: startTransaction() → getSession(key) → commitTransaction(key) / abortTransaction(key).
Express — request/response helpers
@wetech-code_official/common-lib/express wires request validation, standardized JSON responses, and route registration around a minimal structural Request/Response type — duck-typed to match Express, not imported from it, so a real Express req/res (or anything with the same shape) works without casts.
Validator — parses a request segment against any schema with a .parse() method (Zod works out of the box) and returns the typed result:
import { validator } from "@wetech-code_official/common-lib/express";
const body = validator.setBody(CreateUserSchema, req); // reassigns req.body
const query = validator.setQuery(ListQuerySchema, req); // reassigns req.query
const params = validator.setParams(IdParamsSchema, req); // reassigns req.params
const headers = validator.setHeaders(HeaderSchema, req); // read-only — does not touch req.headers
const cookies = validator.setCookies(CookieSchema, req); // parsed from the raw Cookie header, no cookie-parser neededsetHeaders/setCookies deliberately don't reassign req.headers/cookies back — headers and cookies commonly carry keys unrelated to your schema (host, user-agent, other cookies) that a narrow schema would otherwise strip.
Routes — describe endpoints as data (AppRoute[]) and let registerRoutes wire validation + handler + middleware:
import {
HttpMethod,
registerRoutes,
type AppRoute,
} from "@wetech-code_official/common-lib/express";
const routes: AppRoute[] = [
{
endpoint: "getUser",
version: "v1",
method: HttpMethod.GET,
path: "/:id",
handler: asyncHandler(getUserHandler),
hasBody: false,
paramsSchema: IdParamsSchema,
headersSchema: HeaderSchema, // optional
cookiesSchema: CookieSchema, // optional
},
];
registerRoutes(app, routes, { basePath: "/internal/user" });Any route with a paramsSchema/querySchema/bodySchema/headersSchema/cookiesSchema automatically gets a validation middleware ahead of the handler; a failed .parse() is forwarded to next(error) for errorHandler to catch.
Responses — sendSuccess/sendApiError write the standard { status, data, errorCode, errorMessages } envelope, and can set response headers/cookies in the same call:
import { sendSuccess } from "@wetech-code_official/common-lib/express";
return sendSuccess(res, {
data: user,
statusCode: 201,
headers: { "X-Resource-Id": user.id },
cookies: {
session: { value: token, httpOnly: true, secure: true, sameSite: "Lax", maxAge: 3600 },
},
});Cookies are serialized to real Set-Cookie headers, one per cookie — never comma-joined, since an Expires date already contains commas that would corrupt a joined value. sendApiError takes the same headers/cookies fields.
Middleware/utilities:
| Export | What it does |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| asyncHandler(handler) | Catches a rejected promise from an async handler and forwards it to next |
| errorHandler | Maps AppError, Zod validation errors, and unknown errors to the standard error envelope |
| notFoundHandler | Standard 404 envelope for unmatched routes |
| requestContext | Stamps req.requestId (from x-request-id or a new UUID) and echoes it as a response header |
| createInternalRouteGuard(apiKey) | Rejects a request missing a matching x-internal-api-key header with a 404 (hides internal routes from the outside) |
| RouteRegistry | In-memory collector of registered AppRoutes, for introspection/tooling |
| getApiRequest(req) | Normalizes a request into the ApiRequest shape from /api |
| sendFileDownload(res, file) | Sets download headers (via getDownloadHeaders) and sends file content |
Module Proxy — ModuleProxyService
For a BFF calling into internal modules over HTTP:
import {
ModuleProxyService,
registerModuleProxyRoute,
} from "@wetech-code_official/common-lib/proxy";
const proxy = new ModuleProxyService({
baseUrl: "http://localhost:4000",
modules: {
user: { name: "user", enabled: true },
},
});
const user = await proxy.callModuleApi<User>({
moduleName: "user",
endpoint: "/api/v1/user/123",
method: "GET",
});registerModuleProxyRoute(app, proxy) mounts a generic /proxy/:moduleName/* passthrough route if you want a catch-all instead of calling callModuleApi directly. Hop-by-hop headers (connection, content-length, host, transfer-encoding) are stripped before forwarding; a non-2xx or { status: "error" } response throws AppError with the module/endpoint in its details.
Internal HTTP client base — InternalApiHttpClient
@wetech-code_official/common-lib/http is a base class for typed internal-module clients — extend it and add methods on top of the protected get/post:
import { InternalApiHttpClient } from "@wetech-code_official/common-lib/http";
class UserClient extends InternalApiHttpClient {
getUser(id: string) {
return this.get<User>(`/api/v1/user/${id}`, "USER_FETCH_FAILED");
}
}Automatically sends x-internal-api-key, unwraps the { status, data, errorCode, errorMessages } envelope, and throws AppError on a non-OK response or an error envelope. get returns null on a 404 instead of throwing.
Errors — AppError
import { AppError } from "@wetech-code_official/common-lib/errors";
throw new AppError("User not found", 404, "USER_NOT_FOUND", { id });errorHandler (from /express), InternalApiHttpClient, and ModuleProxyService all key off AppError.statusCode/.code/.details to produce a consistent error response.
Config — createJsonConfigSource
Loads a JSON config file or URL pointed to by an env var, with a .env-style pre-loader:
import { createJsonConfigSource } from "@wetech-code_official/common-lib/config";
const configSource = createJsonConfigSource({ envKey: "config_url_bff", envFilePath: ".env" });
const config = await configSource.load(); // fetch() if the pointer is http(s), otherwise reads the file.get() is the synchronous equivalent but throws if the pointer resolves to a URL. Values already present in process.env always win over the .env file (??=), so real environment config isn't silently overridden.
API shapes — /api
ApiRequest/ApiResponse are the request/response envelope types used across the BFF and internal modules. createApiSuccessResponse/createApiErrorResponse build them, createApiRequest is an identity helper for typing a normalized request, and getDownloadHeaders builds content-type/content-disposition/content-length for a file download.
SQL enums — /sql
DatabaseNames (postgres/mysql/mongodb/sqlite) and DefaultTimeout (query/transaction/reconnect-delay/max-retries/max-pool-size defaults) — the constants PostgresPoolOps falls back to when a config value isn't provided, exported for consumers who want the same defaults.
Scripts
npm run type-check # tsc --noEmit
npm run lint # eslint . --max-warnings=0
npm run lint:fix
npm run format
npm run format:check
npm run build # tsup -> dist/
npm run dev # ts-node examples/test.ts
npm run clean # rm -rf distNo test suite exists in this package.
