npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nodecore-kit

v0.4.0

Published

Backend foundation SDK for Node.js services

Downloads

48

Readme

nodecore-kit

A modular backend SDK for Node.js services.

Provides infrastructure helpers, utilities, and microservice building blocks in a clean, scalable, and framework-agnostic way.

View on npm


📦 Features

Infrastructure

  • Redis — get/set/expire, scan, hash ops, auth cache helpers
  • SQS — enqueue, long-poll dequeue, DLQ support, graceful stop
  • S3 — upload, download, stream, copy, signed URLs
  • Cron — job scheduler with human-readable shorthands, overlap protection, per-job status tracking, and graceful shutdown
  • Mailer — provider-agnostic email adapter with SMTP, Resend, and SendGrid support
  • (Future: Kafka, etc.)

HTTP Utilities

  • makeRequest — typed, generic fetch wrapper with retry and timeout
  • Pagination helpers

Core Utilities

  • uuid — binary/string conversion, generation, FIFO support, validation
  • String utilities — camelCase, snakeCase, kebabCase, pascalCase, truncate, maskString, and more
  • Validator utilities — isEmail, isURL, isUUID, isEmpty, isNil, and more
  • Object utilities — flattenObject, unflattenObject
  • Async utilities — sleep, retry, timeout, debounce, throttle, memoize, once

Security

  • JWT — encode, decode, inspect, expiry helpers via jwtService
  • Hashing — bcrypt passwords, HMAC signing, SHA fingerprinting, secure token generation via hashService

Validation

  • joiMiddleware — Express middleware for body/params/query/headers/files
  • joiValidate — inline validator with full type inference

Logging

  • WinstonLogger — structured logging, file transports, child loggers, pretty dev output
  • Logger interface compatible with console or any custom logger

Error Handling

  • ValidationError
  • AuthenticationError
  • NotFoundError
  • ServerError

⚡ Installation

npm install nodecore-kit
# or
yarn add nodecore-kit

🔌 Usage Examples

import {
  uuid,
  hashService,
  jwtService,
  joiMiddleware,
  joiValidate,
  makeRequest,
  retry,
  debounce,
  SQS,
  S3,
  Redis,
  Cron,
  Mailer,
  SmtpProvider,
  ResendProvider,
  SendGridProvider,
  WinstonLogger,
} from "nodecore-kit";

UUID

// Generate
const id = uuid.get();        // v4 (default)
const id = uuid.get("v1");    // v1 time-based

// Binary conversion (optimised for MySQL storage)
const binary = uuid.toBinary("550e8400-e29b-41d4-a716-446655440000");
const str    = uuid.toString(binary);

// Bulk conversion on objects
const record = uuid.manyToString(dbRow, ["id", "userId"]);
const row    = uuid.manyToBinary(record, ["id", "userId"]);

// Validate
uuid.isValid("550e8400-e29b-41d4-a716-446655440000"); // true

Hashing

// Passwords — bcrypt
const hashed = await hashService.hash("myPassword");
const match  = await hashService.compare("myPassword", hashed);

// Password reset / email verification tokens
const { token, hashed } = hashService.generateHashedToken();
await db.user.update({ resetToken: hashed });       // store hash
await email.send({ resetLink: `?token=${token}` }); // send raw token to user

// On reset — hash incoming token and compare
const incoming = hashService.sha256(req.body.token);
const isValid  = incoming === user.resetToken;

// Webhook signature verification
const sig   = hashService.hmac(payload, process.env.WEBHOOK_SECRET!);
const valid = hashService.verifyHmac(payload, secret, req.headers["x-signature"] as string);

// Content fingerprinting / cache keys (not for passwords)
const fingerprint = hashService.sha256("some content");

JWT Service

// Encode
const token = await jwtService.encode({
  data: { userId: 123, role: "admin" },
  secretKey: process.env.JWT_SECRET!,
  expiresIn: "7d",
});

// Decode + verify
const payload = await jwtService.decode<{ userId: number }>({
  token,
  secretKey: process.env.JWT_SECRET!,
});

// Inspect without verifying (safe — never use for auth)
const claims = jwtService.inspect<{ userId: number }>(token);

// Expiry helpers
const expiry    = jwtService.getExpiry(token); // Date | null
const isExpired = jwtService.isExpired(token); // boolean

// Multi-service validation with issuer/audience
const token = await jwtService.encode({
  data: { userId: 1 },
  secretKey,
  issuer: "auth-service",
  audience: "api-service",
});

Joi Validation

import Joi from "joi";

const createUserSchema = Joi.object({
  name:  Joi.string().required(),
  email: Joi.string().email().required(),
});

// As Express middleware
router.post(
  "/users",
  joiMiddleware({
    body:   { schema: createUserSchema },
    params: { schema: Joi.object({ id: Joi.string().uuid().required() }) },
    query:  { schema: paginationSchema, options: { allowUnknown: true } },
  }),
  createUser,
);

// Inline / direct validation
const dto = joiValidate<CreateUserDto>({
  schema: createUserSchema,
  data: req.body,
});

HTTP Requests

// Simple GET
const user = await makeRequest<User>({ url: "/api/users/1" });

// POST with typed body
const post = await makeRequest<Post, CreatePostDto>({
  url: "/api/posts",
  method: "POST",
  data: { title: "Hello", body: "World" },
  token: "my-jwt-token",
});

// With retry + timeout
const data = await makeRequest<Data>({
  url: "/api/slow-endpoint",
  timeout: 5000,
  retries: 3,
});

Async Utilities

// sleep
await sleep(1000);

// retry with exponential backoff
const data = await retry(
  () => fetchUser(id),
  { retries: 3, delay: 500, exponential: true, onError: (err, attempt) => logger.warn(`Attempt ${attempt} failed`, { err }) }
);

// timeout
const data = await timeout(fetchUser(id), 5000);

// debounce with cancel/flush
const search = debounce((query: string) => fetchResults(query), 300);
search("hello");
search.cancel();
search.flush("hello");

// throttle with trailing edge
const onScroll = throttle(() => updatePosition(), 100, { trailing: true });

// memoize (works with async functions too)
const getUser = memoize((id: number) => fetchUser(id));
await getUser(1); // fetches
await getUser(1); // returns cached — getUser.clear() to reset

// once — run exactly one time
const init = once(() => setupDatabase());
await init(); // runs
await init(); // returns cached result, does not run again

String Utilities

capitalize("hello world")              // "Hello world"
camelCase("hello_world")               // "helloWorld"
pascalCase("hello_world")              // "HelloWorld"
snakeCase("helloWorld")                // "hello_world"
kebabCase("helloWorld")                // "hello-world"
splitWords("helloWorld")               // ["hello", "world"]
truncate("Hello, world!", 8)           // "Hello..."
truncate("Hello, world!", 8, " →")     // "Hello →"
maskString("4111111111111234")         // "************1234"
isBlank("   ")                         // true
reverse("hello")                       // "olleh"
countOccurrences("hello world", "l")   // 3
normalizeWhitespace("  hello   world") // "hello world"

Validator Utilities

isEmail("[email protected]")   // true
isURL("https://example.com")  // true  (http/https only)
isUUID("550e8400-...")         // true
isObject({ a: 1 })            // true
isArray([1, 2, 3])            // true
isString("hello")             // true
isNumber(42)                  // true  (excludes NaN, Infinity)
isInteger(3)                  // true
isPositive(5)                 // true
isNegative(-1)                // true
isBoolean(false)              // true
isDate(new Date())            // true
isJSON('{"a":1}')             // true
isNil(null)                   // true
isEmpty([])                   // true
isEmpty({})                   // true
isEmpty("  ")                 // true

Object Utilities

// Flatten
flattenObject({ a: { b: { c: 1 } } })
// → { "a.b.c": 1 }

flattenObject({ a: { b: 1 } }, { separator: "_" })
// → { "a_b": 1 }

// Unflatten
unflattenObject({ "a.b.c": 1 })
// → { a: { b: { c: 1 } } }

Redis

const redis = new Redis("redis://localhost:6379");
await redis.start();

// Core ops
await redis.set("key", { foo: "bar" });
await redis.setEx("key", { foo: "bar" }, "1 hour");
const value = await redis.get<MyType>("key");
await redis.delete("key");
await redis.exists("key");
await redis.ttl("key");
await redis.expire("key", "30 minutes");

// Counters
await redis.increment("rate:user:123");           // 1, 2, 3...
await redis.increment("rate:user:123", "1 hour"); // sets TTL on first create
await redis.decrement("rate:user:123");

// Hash ops
await redis.hset("user:1", { name: "Alice", role: "admin" });
const name = await redis.hget("user:1", "name");
const all  = await redis.hgetAll<User>("user:1");
await redis.hdel("user:1", "role");

// Pattern ops (safe — uses SCAN not KEYS)
const keys    = await redis.scan("session:*");
const deleted = await redis.deleteByPattern("session:*");

// Auth cache helpers
await redis.cacheUser(user, "1 day");
const cached = await redis.getCachedUser(userId);
await redis.updateAuthData(userId, "permissions", "admin:write", "ADD");
await redis.updateAuthData(userId, "permissions", "admin:write", "REMOVE");

// Flush (throws in production unless forced)
await redis.flush();      // throws in production
await redis.flush(true);  // override

SQS

const sqs = new SQS({
  region: "us-east-1",
  accessKeyId: process.env.AWS_KEY!,
  secretAccessKey: process.env.AWS_SECRET!,
});

// Enqueue
await sqs.enqueue({
  queueUrl: "https://sqs.us-east-1.amazonaws.com/1234/my-queue",
  message: { event: "user.created", userId: 1 },
});

// FIFO queue
await sqs.enqueue({
  queueUrl: "https://sqs.us-east-1.amazonaws.com/1234/my-queue.fifo",
  message: { event: "order.placed" },
  messageGroupId: "orders",
  messageDeduplicationId: uuid.get(),
});

// Dequeue (long-polls until stop() is called)
sqs.dequeue({
  queueUrl: "https://sqs.us-east-1.amazonaws.com/1234/my-queue",
  consumerFunction: async (message) => {
    await processMessage(message);
  },
  dlqUrl: "https://sqs.us-east-1.amazonaws.com/1234/my-dead-letter-queue",
  useRedrivePolicy: false,
});

// Graceful shutdown
process.on("SIGTERM", () => sqs.stop());

S3

const s3 = new S3({
  region: "us-east-1",
  accessKeyId: process.env.AWS_KEY!,
  secretAccessKey: process.env.AWS_SECRET!,
  defaultBucket: "my-default-bucket",
});

// Upload — returns { bucket, key, url }
const result = await s3.upload({
  key: "avatars/user-1.png",
  body: buffer,
  contentType: "image/png",
  metadata: { uploadedBy: "user-1" },
});

// Download as Buffer
const buffer = await s3.download({ key: "avatars/user-1.png" });

// Stream (preferred for large files)
const stream = await s3.stream({ key: "videos/clip.mp4" });

// Copy within or across buckets
await s3.copy({
  sourceKey: "uploads/tmp.png",
  destinationKey: "avatars/user-1.png",
});

// Delete / exists
await s3.delete({ key: "avatars/user-1.png" });
const exists = await s3.exists({ key: "avatars/user-1.png" });

// Signed URLs
const downloadUrl = await s3.getSignedDownloadUrl({ key: "report.pdf", expiresIn: 3600 });
const uploadUrl   = await s3.getSignedUploadUrl({ key: "avatar.png", contentType: "image/png" });

// Bucket preset — great for scoping per feature
const avatars = s3.bucket("user-avatars");
await avatars.upload({ key: "user-1.png", body: buffer });
await avatars.getSignedDownloadUrl({ key: "user-1.png" });

Cron

const cron = new Cron(logger); // logger is optional

// Register with human shorthand
cron.register({
  name: "send-digest",
  schedule: "every day at noon",
  timezone: "America/New_York",
  handler: async () => {
    await sendDigestEmails();
  },
});

// Register with raw cron expression
cron.register({
  name: "sync-inventory",
  schedule: "*/15 * * * *",
  handler: async () => { await syncInventory(); },
});

// Run immediately on registration
cron.register({
  name: "warm-cache",
  schedule: "every hour",
  runOnInit: true,
  handler: async () => { await warmCache(); },
});

// Manual trigger (e.g. from an admin endpoint)
await cron.run("send-digest");

// Stop / start individual jobs
cron.stop("sync-inventory");
cron.start("sync-inventory");

// Replace a job's schedule at runtime
cron.replace({ name: "send-digest", schedule: "every 30 minutes", handler });

// Introspect
cron.status("send-digest"); // { name, schedule, running, lastRun, executionCount, errorCount }
cron.statusAll();            // all jobs

// Graceful shutdown
process.on("SIGTERM", () => cron.stopAll());

Supported shorthands:

| Shorthand | Expression | |---|---| | "every minute" | * * * * * | | "every 5 minutes" | */5 * * * * | | "every 15 minutes" | */15 * * * * | | "every 30 minutes" | */30 * * * * | | "every hour" | 0 * * * * | | "every 6 hours" | 0 */6 * * * | | "every 12 hours" | 0 */12 * * * | | "every day" | 0 0 * * * | | "every day at noon" | 0 12 * * * | | "every week" | 0 0 * * 0 | | "every month" | 0 0 1 * * |


Mailer

// SMTP (Nodemailer)
const mailer = new Mailer(
  new SmtpProvider({
    host: "smtp.gmail.com",
    auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },
    defaultFrom: "[email protected]",
  }),
  {},
  logger, // optional
);

// Resend
const mailer = new Mailer(
  new ResendProvider({
    apiKey: process.env.RESEND_KEY!,
    defaultFrom: "[email protected]",
  }),
);

// SendGrid
const mailer = new Mailer(
  new SendGridProvider({ apiKey: process.env.SENDGRID_KEY! }),
  { defaultFrom: "[email protected]" },
);

// Send
await mailer.send({
  to: "[email protected]",
  subject: "Welcome!",
  html: "<h1>Hello Alice</h1>",
  text: "Hello Alice", // plain text fallback
});

// Multiple recipients + attachments
await mailer.send({
  to: ["[email protected]", "[email protected]"],
  cc: "[email protected]",
  subject: "Your invoice",
  html: "<p>Please find your invoice attached.</p>",
  attachments: [{ filename: "invoice.pdf", content: pdfBuffer, contentType: "application/pdf" }],
});

// Runtime provider swap (fallback)
try {
  await mailer.send(mail);
} catch {
  mailer.setProvider(new SendGridProvider({ apiKey: process.env.SENDGRID_KEY! }));
  await mailer.send(mail);
}

// Preview mode — logs instead of sending (auto-enabled in development)
const mailer = new Mailer(provider, { previewMode: true });

Logger

const logger = new WinstonLogger({
  service: "auth-service",
  level: "debug",
  file: {
    path: "logs/combined.log",
    errorPath: "logs/error.log",
  },
});

logger.info("Server started", { port: 3000 });
logger.error("DB connection failed", err); // stack trace preserved

// Child logger — attach request context to all logs in scope
app.use((req, res, next) => {
  req.log = logger.child({ requestId: req.id, path: req.path });
  next();
});

req.log.info("Request received");
// → { requestId: "abc-123", path: "/users", message: "Request received" }

// Runtime level control
logger.setLevel("debug");
logger.isLevelEnabled("debug"); // true

🏗️ Architecture Principles

| Layer | Description | |---|---| | Core | Pure utilities, errors, and types. Independent from adapters or transports. | | Adapters | External services (SQS, Redis, S3, Cron, Mailer). Depend only on core. | | Transport | HTTP layers. May depend on core. | | Security | JWT, hashing. Depends only on core. | | Logger | Optional, adapter-friendly, injected as a dependency. |

No global environment reads inside adapters — all configuration is passed via constructor.


🌱 Design Goals

  • Simplicity — Easy to pick up and integrate into any project.
  • Modularity — Pick and use only what you need.
  • Scalable — Built for microservices and multi-service architectures.
  • Plug-n-Play — Default logging works with console, but advanced logging can be injected.
  • Framework-Agnostic — Works with Express, Fastify, NestJS, or bare Node.js.
  • Type-Safe — Generics throughout so you get full TypeScript inference without casting.