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

@d2sutils/shopperv2

v1.7.3

Published

This is a simple utility service that provides a set of utility functions for the shopper service.

Readme

Shopper Utils v2

Version License TypeScript

🇺🇦 Українська версія | 🇬🇧 English Version


🇬🇧 English Version

Description

Shopper Utils v2 is a utility service that provides a set of useful functions for working with the Shopper service. The library is written in TypeScript and includes various services for working with users, locations, caching, and other functionality.

Key Features

  • 👥 User Management - working with users, clients, shoppers, and groups
  • 📍 Locations - retrieving location and location group information
  • 💾 Caching - Redis integration for fast data access
  • 📧 Notifications - sending messages and notifications
  • 🔧 Formatting - utilities for data formatting
  • 📦 Archiving - working with ZIP archives
  • 📊 Logging - integrated logging system
  • 📚 References - powerful ReferenceManager for multilingual dictionaries
  • 🐛 Debug utilities - tools for debugging, performance measurement, and smart logging
  • 🔀 Auto-Batch - automatic splitting of long GET requests into batches with response merging

Quick Navigation

Installation

npm install @d2sutils/shopperv2

Quick Start

import { instanceAppV2, user, location, cache } from '@d2sutils/shopperv2';

// Initialize the application
instanceAppV2('your-proxy-url', 'your-secret-key', 'redis://localhost:6379', true);

// Working with users
const users = await user.users({ filter: {}, limit: 10 });
const isClient = await user.isClient(123);
const userName = await user.getNameById(123);

// Working with locations
const locations = await location.all({ filter: {}, limit: 25 });
const groups = await location.groups({ filter: {}, limit: 25 });

// Working with cache
await cache.set('key', 'value', 3600);
const value = await cache.get('key');

// Working with references
import { ReferenceManager } from '@d2sutils/shopperv2';
await ReferenceManager.setReference('countries', countriesData);
const countryName = await ReferenceManager.getNameById('countries', 1, 'en');

API Documentation

User Service

// Get all users
await user.users({ filter: {}, limit: 25 });

// Get clients
await user.clients({ filter: {}, limit: 25 });

// Get shoppers
await user.shoppers({ filter: {}, limit: 25 });

// Check if user is a client
await user.isClient(userId: number | number[]);

// Check if user is a manager
await user.isManager(userId: number | number[]);

// Get user by ID
await user.getById(id: number);

// Get current user
await user.me(token: string);

Location Service

// Get all locations
await location.all({ filter: {}, limit: 25, id?: number });

// Get location groups
await location.groups({ filter: {}, parent_id?: number, limit: 25 });

Cache Service

// Set value in cache
await cache.set(key: string, value: string, ttl?: number);

// Get value from cache
await cache.get(key: string);

// Check if key exists
await cache.has(key: string);

// Delete key
await cache.del(key: string);

// Clear service cache
await cache.clearService(service: string);

// Clear all cache
await cache.flushAll();

Notification Service

// Send notification
await notification.send({
    action: 'email', // action type
    user_id: '123', // user ID
    contact: '[email protected]', // contact
    communication_id: 'comm_123', // communication ID
    communication_name: 'Welcome Email', // communication name
    subject: 'Welcome!', // subject
    message: 'Thank you for registration', // message
    params: { name: 'John' }, // additional parameters
    header: { 'X-Custom': 'value' } // additional headers
});

Format Service

// Apply filter to model
const filteredModel = format.applyFilter({
    model: MyModel,
    filter: { name: 'John', age: 25 },
    config: {
        like: ['name'], // fields for LIKE search
        like_number: ['age'], // numeric fields for LIKE
        in: ['status'], // fields for IN search
        custom: {
            customField: (value) => `custom_${value}`
        }
    }
});

// Format request body
const formattedBody = format.body({
    name: 'John',
    active: 'true', // will be converted to boolean
    data: '{"key": "value"}', // will be parsed as JSON
    items: ['item1', 'item2']
});

Response Utils

A utility for formatting standardized API responses with support for various HTTP statuses.

Basic methods:

// Create success response
const successResponse = responseUtils.success({
    data: { id: 1, name: 'John' },
    message: 'Operation completed successfully'
});

// Create error response
const errorResponse = responseUtils.error({
    message: 'Validation error',
    status: 400
});

// Create response with data and notifications
const dataResponse = responseUtils.sendData({
    data: { users: [] },
    notifications: { unread: 5 }
});

// Create paginated response
const paginatedResponse = responseUtils.paginate({
    data: [{ id: 1 }, { id: 2 }],
    total: 100,
    limit: 10,
    page: 1,
    other: { customField: 'value' },
    notifications: { unread: 3 }
});

HTTP status codes (convenience wrappers):

// 201 Created - for created resources
const createdResponse = responseUtils.created({
    data: { id: 123, name: 'New Resource' },
    message: 'Resource created successfully'
});

// 204 No Content - for successful operations with no content
const noContentResponse = responseUtils.noContent();

// 400 Bad Request - for invalid requests
const badRequestResponse = responseUtils.badRequest({
    message: 'Invalid request parameters',
    errors: { field: 'Field is required' }
});

// 401 Unauthorized - for unauthorized requests
const unauthorizedResponse = responseUtils.unauthorized({
    message: 'Authentication required'
});

// 403 Forbidden - for forbidden operations
const forbiddenResponse = responseUtils.forbidden({
    message: 'You do not have permission'
});

// 404 Not Found - for missing resources
const notFoundResponse = responseUtils.notFound({
    message: 'User not found',
    resource: 'User'
});

// 422 Validation Error - for validation errors
const validationErrorResponse = responseUtils.validationError({
    message: 'Validation failed',
    errors: {
        email: 'Email is invalid',
        password: 'Password must be at least 8 characters'
    }
});

// 500 Internal Server Error - for server errors
const serverErrorResponse = responseUtils.serverError({
    message: 'An unexpected error occurred',
    error: errorObject // Shown only in development
});

Additional utilities:

// Add metadata to response
const responseWithMeta = responseUtils.withMeta({
    data: { users: [] },
    meta: {
        version: '1.0.0',
        apiVersion: 'v2',
        requestId: 'req-123'
    }
});
// Result: { users: [], meta: { timestamp: '2024-...', version: '1.0.0', ... } }

Usage examples in controllers:

import { responseUtils } from "@d2sutils/shopperv2";
import { Request, Response } from "express";

// Creating a resource
export const createUser = async (req: Request, res: Response) => {
    const user = await userService.create(req.body);
    return res.status(201).json(responseUtils.created({ data: user }));
};

// Retrieving a resource
export const getUser = async (req: Request, res: Response) => {
    const user = await userService.findById(req.params.id);
    if (!user) {
        return res.status(404).json(responseUtils.notFound({ resource: 'User' }));
    }
    return res.json(responseUtils.success({ data: user }));
};

// Validation
export const updateUser = async (req: Request, res: Response) => {
    const { error, value } = schema.validate(req.body);
    if (error) {
        return res.status(422).json(responseUtils.validationError({
            errors: error.details.reduce((acc, detail) => {
                acc[detail.path[0]] = detail.message;
                return acc;
            }, {})
        }));
    }
    // ... processing
};

// Pagination
export const getUsers = async (req: Request, res: Response) => {
    const { page = 1, limit = 10 } = req.query;
    const { data, total } = await userService.findAll({ page, limit });
    return res.json(responseUtils.paginate({ data, total, page, limit }));
};

ZIP Utils

// Validate postal code for different countries
const isValidZip = validateZip('UA', '12345'); // true for Ukraine
const isValidZipDE = validateZip('DE', '12345'); // true for Germany
const isValidZipGB = validateZip('GB', 'SW1A1AA'); // true for United Kingdom

// Supported countries: AL, AD, AT, BY, BE, BA, BG, HR, CZ, CY, DK, EE, FI, FR, DE, GR, HU, IS, IE, IT, LV, LI, LT, LU, MK, MT, MD, MC, NL, NO, PL, PT, RO, RU, SM, RS, SK, SI, ES, SE, CH, TN, TR, UA, GB, VA

Action Logger

// Get singleton logger instance (uses shared Redis connection)
const logger = ActionLogger.getInstance();

// Log user actions
await logger.logUserAction({
    request_type: 'POST',
    url: '/api/users',
    user_id: 123,
    origin: 'https://example.com',
    referrer: 'https://google.com',
    payload: { name: 'John' },
    session_id: 'session_123',
    response: { success: true }
});

// Disconnect not needed - uses shared connection
// await logger.disconnect(); // no longer needed

ActionLogger Features:

  • ✅ Uses Singleton pattern
  • ✅ Uses shared Redis connection with Cache Service
  • ✅ Automatically initializes when calling getInstance()
  • ✅ No separate Redis configuration needed - uses settings from instanceAppV2

API Utils

// Execute GET request
const data = await api.get('user', 'users', { limit: 10 }, {
    useCache: true,
    cache_options: { ttl: 3600, name: 'users_cache' }
});

// Execute POST request
const result = await api.post('user', 'create', { name: 'John' }, {}, {
    headers: { 'Content-Type': 'application/json' }
});

// Execute PUT request
const updated = await api.put('user', 'update/123', { name: 'Jane' });

// Universal request
const response = await api.request('GET', 'location', 'all', {}, { limit: 25 });

Auto-Batch for long GET requests

The autoBatch option automatically splits GET requests with long query parameters into several batches and merges the responses into a single result. It is disabled by default — enable it explicitly on the call.

Problem: some GET requests contain very long parameters (for example, filter[id]=769,770,771,...,10840 — hundreds of IDs), which leads to an HTTP 431 error (Request Header Fields Too Large).

Solution: when autoBatch is enabled, the utility automatically:

  1. Analyzes all query parameters and finds the longest one (any key, not just id)
  2. If the URL exceeds the limit — splits only the longest parameter into batches, leaving the other parameters unchanged
  3. Executes parallel requests (up to 3 simultaneously)
  4. Recursively merges the responses into a single result
  5. If the server returns 431 — automatically switches to batching (fallback)

Basic usage:

// Regular request — without batching (as before)
const data = await api.get('locations', 'shops', {
    filter: { id: '769,770,...,10840', client_id: 1 },
    limit: 1000,
});

// With automatic batching
const data = await api.get('locations', 'shops', {
    filter: { id: '769,770,...,10840', client_id: 1 },
    limit: 1000,
}, { autoBatch: true });

// With a custom batch size (default is 100)
const data = await api.get('locations', 'shops', {
    filter: { id: '769,770,...,10840', client_id: 1 },
    limit: 1000,
}, { autoBatch: true, batchSize: 50 });

How response merging works:

// If the API returns an array — concatenation
// [1, 2, 3] + [4, 5, 6] → [1, 2, 3, 4, 5, 6]

// If the API returns an object with data and pagination:
// { data: [...], pagination: { total: 100, pageSize: 25, ... } }
// + { data: [...], pagination: { total: 50, pageSize: 25, ... } }
// → { data: [...all items...], pagination: { total: 150, totalPages: 6, pageSize: 25, ... } }

// For other objects — recursive deep-merge:
// arrays → concatenation
// objects → recursively by keys
// primitives → value from the last response

Combining with cache:

const data = await api.get('locations', 'shops', {
    filter: { id: longIdList, client_id: 1 },
    limit: 1000,
}, {
    autoBatch: true,
    useCache: true,
    cache_options: { ttl: 3600, name: 'shops_cache' },
});

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | autoBatch | boolean | false | Enable automatic batching of long requests | | batchSize | number | 100 | Number of items in a single batch |

Features:

  • Analyzes any query parameter (not only filter[id]), including nested keys
  • Default URL limit — 2000 characters
  • Parallel batch execution (up to 3 simultaneously)
  • Automatic fallback on HTTP 431 even if the URL seemed short
  • pagination.total — summed, pagination.totalPages — recalculated
  • Works with any value format: array, comma-separated string

Debug Utils

import { debug } from '@d2sutils/shopperv2';

// Measure promise execution time
const result = await debug.measurePromise(
    user.users({ limit: 10 }),
    'Fetch users'
);
// Outputs (if debug=true): ⏱️  Fetch users executed in 123.45ms

// Process tracking
debug.start('data_processing');
// ... some code ...
debug.end('data_processing');
// Always outputs: ⏱️  Process "data_processing" took 123.45ms

// Smart logs
debug.log('This message is visible only if debug=true');
debug.info('Informational message (only when debug=true)');
debug.debug('Debug message (only when debug=true)');

// Warnings and errors are always visible
debug.warn('Warning - always visible');
debug.error('Error - always visible');

Debug Utils Features:

  • measurePromise() - automatically measures promise execution time
  • start() / end() - track the execution time of any process
  • ✅ Smart logs - log(), info(), debug() are shown only when debug=true
  • warn() and error() are always visible for important messages
  • ✅ Automatic time formatting with millisecond precision

Helper Utilities

// Free port (kill processes occupying it)
await ensurePortFree(3000, 5); // port 3000, 5 retries

// Generate color from string
const color = stringToColor('user123'); // generates hex color
const softColor = stringToColor('user123', true); // soft color

// Sleep execution
await sleep(1000); // pause for 1 second

// Normalize number to string
const normalized = normalNumber(123.456); // "123.46"
const normalizedInt = normalNumber(123); // "123"

// Remove undefined values from object
const cleaned = removeUndefinedDeep({
    name: 'John',
    age: undefined,
    data: { value: 'test', empty: undefined }
}); // { name: 'John', data: { value: 'test' } }

// Slice string with ellipsis
const short = strSlice('Long text for slicing', 10); // "Long text..."

// Filter by date
const items = [
    { name: 'Event 1', date_start: '2024-01-01', date_end: '2024-01-31' },
    { name: 'Event 2', date_start: '2024-02-01', date_end: '2024-02-28' }
];
const filtered = filterByDate(items, '2024-01-15'); // returns Event 1

ReferenceManager - Multilingual Dictionaries

// Create multilingual countries dictionary
const countries = [
    {
        id: 1,
        code: "UA",
        name_en: "Ukraine",
        name_ru: "Украина", 
        name_uk: "Україна"
    }
];
await ReferenceManager.setReference('countries', countries);

// Create dictionary with different field patterns
const regions = [
    {
        id: 3767,
        country: "UA",
        regionEn: "Khersonska Oblast",
        regionRu: "Херсонская область",
        regionUk: "Херсонська область"
    }
];
await ReferenceManager.setReference('regions', regions);

// Create regular users dictionary
const users = [
    {
        id: 1,
        name: "John Smith",
        email: "[email protected]",
        role: "admin"
    }
];
await ReferenceManager.setReference('users', users);

// Get data from multilingual dictionaries
const countryNameUk = await ReferenceManager.getNameById('countries', 1, 'uk'); // "Україна"
const countryNameEn = await ReferenceManager.getNameById('countries', 1, 'en'); // "Ukraine"

// Work with different field patterns (regionEn, regionUk)
const regionNameUk = await ReferenceManager.getLocalizedField('regions', 3767, 'uk', 'region');
const regionNameEn = await ReferenceManager.getLocalizedField('regions', 3767, 'en', 'region');

// Work with regular dictionaries
const userName = await ReferenceManager.getNameById('users', 1, 'name'); // "John Smith"
const userEmail = await ReferenceManager.getFieldById('users', 1, 'email'); // "[email protected]"

// Search in dictionaries
const searchResults = await ReferenceManager.search('countries', 'Ukraine', {
    field: 'name',
    language: 'en'
});

// Fast search by ID (optimized - uses Redis indexes)
const city = await ReferenceManager.search('cities', '123', { field: 'id' });
const cities = await ReferenceManager.search('cities', '123, 456, 789', { field: 'id' });

// Fast search by array of IDs through indexes (O(1) for each ID)
const citiesByIds = await ReferenceManager.findByIds('cities', [123, 456, 789]);

// Search by array of values for any field
const citiesByCode = await ReferenceManager.findByField('cities', 'code', ['NYC', 'LA']);
const usersByEmail = await ReferenceManager.findByField('users', 'email', ['[email protected]']);
// For 'id' field automatically uses fast indexes
const citiesByIds2 = await ReferenceManager.findByField('cities', 'id', [123, 456]);

// Batch data retrieval
const countryNames = await ReferenceManager.getBatchNames('countries', [1, 2, 3], 'uk');
const userItems = await ReferenceManager.getBatchItems('users', [1, 2]);

// Dictionary metadata
const isMultilingual = await ReferenceManager.isMultilingual('countries'); // true
const languages = await ReferenceManager.getAvailableLanguages('countries'); // ["en", "ru", "uk"]
const fields = await ReferenceManager.getAvailableFields('users'); // ["id", "name", "email", "role"]

// Dictionary management
const allReferences = await ReferenceManager.listReferences();
await ReferenceManager.updateItem('users', 1, { role: 'manager' });
await ReferenceManager.deleteItem('users', 1);
await ReferenceManager.deleteReference('old_reference');

Supported language field patterns:

  • name_en, name_ru, name_uk (underscore)
  • nameEn, nameRu, nameUk (camelCase)
  • regionEn, regionUk, titleEn (camelCase with prefix)
  • NameEn, RegionUk, TitleEn (PascalCase)

Main methods:

  • setReference() - create/update dictionary
  • getReference() - get entire dictionary
  • getById() - get item by ID
  • getNameById() - get name with automatic pattern recognition
  • getLocalizedField() - get language field
  • search() - search by text (optimized for ID search)
  • findByIds() - fast search by array of IDs through Redis indexes (O(1))
  • findByField() - search by array of values for any field
  • getBatchNames() - batch name retrieval
  • isMultilingual() - check if multilingual

Performance optimizations:

  • findByIds() - uses direct Redis indexes for fast ID search
  • search() with field: 'id' - automatically uses indexes instead of loading entire dictionary
  • findByField() with fieldName: 'id' - automatically uses fast indexes

Configuration

The library supports the following configuration parameters:

  • proxy - proxy server URL
  • secret - secret key for authentication
  • redis_uri - Redis connection URI (optional)
  • debug - debug mode (optional)

Dependencies

  • Node.js 16+
  • Redis 7+ (for caching)
  • TypeScript 5.4+

Redis Architecture

The library uses a single Redis connection for all services:

  • Cache Service - singleton for data caching
  • ActionLogger - singleton, uses shared connection
  • API Utils - uses Cache Service for request caching
  • getRedis() - singleton for working with Redis and BullMQ workers
  • 🔧 Configured once through instanceAppV2()
  • 🚀 Optimized for performance and resources

ServerService - Managing Express Server and Workers

ServerService is a powerful class for creating and managing Express servers with support for workers, databases, caching, and other features.

Basic usage

import { ServerService } from "@d2sutils/shopperv2";
import { IDefaultConfig } from "@d2sutils/shopperv2";
import routes from "./api/routes";

const config: IDefaultConfig = {
    name: "my-service",
    app_url: "http://localhost:3000",
    node: process.env.NODE_ENV || "development",
    port: process.env.PORT || 3000,
    proxy: process.env.PROXY_URI || "http://localhost:3000",
    pg_uri: process.env.PG_URI || "postgres://localhost:5432",
    pg_logging: process.env.PG_LOGGING === "true" || false,
    pg_connect: process.env.PG_CONNECT || "false",
    secret: process.env.SECRET_KEY || "secret",
    redis_uri: process.env.REDIS_URI || "redis://localhost:6379",
};

const server = new ServerService(config, routes, {
    healthCheck: true,
    errorHandling: true,
});

server.start().catch((error) => {
    console.error("Error starting service", error);
    process.exit(1);
});

Options configuration

const server = new ServerService(config, routes, {
    // Hooks for various initialization stages
    hooks: {
        beforeInit: async () => {
            console.log("Before initialization");
        },
        afterInit: async () => {
            console.log("After initialization");
        },
        beforeListen: async () => {
            console.log("Before server starts listening");
        },
        afterListen: async () => {
            console.log("After server started");
            // Cron jobs or other modules can be imported here
            await import("./cron/index.cron");
        },
    },

    // Database initialization
    database: {
        initialize: async () => {
            await AppDataSource.initialize()
                .then(() => {
                    console.log("Data Source has been initialized!");
                })
                .catch((error) => {
                    console.error("Error during Data Source initialization", error);
                    throw error;
                });
        },
    },

    // Redis initialization
    redis: {
        initialize: () => {
            // Redis initialization (for example, creating workers)
            getRedis(); // Or other logic
        },
    },

    // Caching with automatic refresh
    cache: {
        functions: [
            async () => {
                // Function for loading cache
                await ensureCacheUsers();
            },
            async () => {
                await ensureCacheProjects();
            },
        ],
        interval: 1 * 60 * 1000, // Refresh every minute
    },

    // Port check before startup
    portCheck: {
        enabled: config.node === "local",
        check: async (port: number | string) => {
            await ensurePortFree(Number(port));
        },
    },

    // Express JSON middleware settings
    expressJson: {
        limit: "10mb",
    },

    // Express URL encoded middleware settings
    expressUrlencoded: {
        limit: "10mb",
        extended: true,
    },

    // Custom middleware
    middleware: [
        languageMiddleware,
        authenticationMiddleware,
    ],

    // Health check endpoint
    healthCheck: true,

    // Global error handling
    errorHandling: true,

    // Logger configuration
    logger: {
        configure: (proxy: string, name: string) => {
            configureLogger(proxy, name);
        },
    },

    // Registering routes in the proxy
    routesList: RoutesList,

    // Custom service URL
    serviceUrl: "http://my-service:3000",

    // Disable automatic dotenv loading
    dotenv: false,
});

Working with workers

The library supports separate startup of BullMQ workers and the Express server with three modes:

Creating workers

import { ServerService, getRedis } from "@d2sutils/shopperv2";
import { Worker } from "bullmq";

// Creating workers using the singleton Redis client
const autosaveWorker = new Worker(
    "answersAutosaveQueue",
    async (job) => {
        // your processing logic
        // getRedis() can be used for regular operations
        const redis = getRedis();
        await redis.get('key');
        await redis.set('key', 'value');
    },
    { connection: getRedis() as any }  // One singleton client for BullMQ
);

const updateWorker = new Worker(
    "updateQueue",
    async (job) => {
        // task processing
    },
    { connection: getRedis() as any }  // The same singleton client
);

// Using in ServerService
const server = new ServerService(config, routes, {
    database: { initialize: async () => { /* ... */ } },
    redis: { initialize: () => { getRedis(); } }, // Initialize Redis singleton
    workers: [updateWorker, autosaveWorker], // Pass the array of workers
});

// The startup mode is determined automatically from arguments or environment variables
server.start();

Startup modes

  • npm run start --only-worker - starts only workers (without Express)
  • npm run start --only-service - starts only Express (without workers)
  • npm run start - starts both Express and workers (default, if workers are passed)
  • WORKER_MODE=only-worker npm run start - alternative way via environment variable

Benefits of the approach

  • One Redis client - used for both BullMQ workers and regular operations
  • Resource savings - one connection instead of two
  • No version conflicts - everyone uses the same client
  • Flexibility - workers can be run separately from the server for scaling

getRedis() for regular operations

import { getRedis } from "@d2sutils/shopperv2";

// Get the singleton Redis client
const redis = getRedis();

// Use for regular operations
await redis.get('key');
await redis.set('key', 'value');
await redis.del('key');
// ... other ioredis operations

Defining routes

Routes can be passed in three ways:

1. Array of Route objects:

import { ServerService, Route } from "@d2sutils/shopperv2";

const routes: Route[] = [
    {
        method: 'get',
        path: '/api/users',
        handler: async (req, res) => {
            res.json({ users: [] });
        },
        type: 'public',
        action: 'R',
        label: 'Get users',
    },
    {
        method: 'post',
        path: '/api/users',
        handler: [authMiddleware, createUserHandler],
        type: 'private',
        action: 'C',
        label: 'Create user',
    },
];

const server = new ServerService(config, routes, options);

2. Express Router:

import express, { Router } from "express";

const router = Router();
router.get('/api/users', getUsersHandler);
router.post('/api/users', createUserHandler);

const server = new ServerService(config, router, options);

3. Express App:

import express from "express";

const app = express();
app.get('/api/users', getUsersHandler);
app.post('/api/users', createUserHandler);

const server = new ServerService(config, app, options);

Worker startup modes

ServerService automatically determines the startup mode from command-line arguments or environment variables:

Modes:

  • only-worker - starts only workers (without the Express server)
  • only-service - starts only the Express server (without workers)
  • both - starts both the server and workers (default, if workers are passed)

Mode determination:

  1. Command-line arguments: --only-worker, --only-service
  2. Environment variable: WORKER_MODE=only-worker
  3. Explicit setting in options: workerMode: 'only-worker'
  4. Default: both (if workers are passed) or only-service (if there are no workers)

Startup examples:

# Workers only
npm run start --only-worker

# Server only
npm run start --only-service

# Both (default)
npm run start

# Via environment variable
WORKER_MODE=only-worker npm run start

Lifecycle management

const server = new ServerService(config, routes, options);

// Start the server
await server.start();

// Get the Express app (after startup)
const app = server.getApp();

// Stop the server (graceful shutdown)
await server.stop();

Graceful shutdown:

// Handle signals for proper termination
process.on('SIGTERM', async () => {
    console.log('SIGTERM received, shutting down gracefully');
    await server.stop();
    process.exit(0);
});

process.on('SIGINT', async () => {
    console.log('SIGINT received, shutting down gracefully');
    await server.stop();
    process.exit(0);
});

Full usage example

import { config as ConfigDotenv } from "dotenv";
ConfigDotenv();

import "reflect-metadata";
import { instanceApp } from "@d2sutils/shopper";
import { ServerService, getRedis } from "@d2sutils/shopperv2";
import routes from "./api/routes";
import { routes as RoutesList } from "./api/routes/routes";
import { config } from "./config";
import { configureLogger } from "@d2sutils/logging";
import { AppDataSource } from "./config/data.source";
import { languageMiddleware } from "./middleware";
import { Worker } from "bullmq";
import {
    ensureCacheCountries,
    ensureCacheProgramTypes,
    ensureCacheProjects,
} from "./utils/cache";

instanceApp(config.proxy, config.secret, config.redis_uri);

// Creating workers
const autosaveWorker = new Worker(
    "answersAutosaveQueue",
    async (job) => {
        // Task processing
        const redis = getRedis();
        await redis.set(`job:${job.id}`, JSON.stringify(job.data));
    },
    { connection: getRedis() as any }
);

const updateWorker = new Worker(
    "updateQueue",
    async (job) => {
        // Task processing
    },
    { connection: getRedis() as any }
);

const server = new ServerService(config, routes, {
    hooks: {
        afterListen: async () => {
            // Import cron jobs after server startup
            await import("./cron/execution.status.crone");
        },
    },
    database: {
        initialize: async () => {
            await AppDataSource.initialize()
                .then(() => {
                    console.warn("Data Source has been initialized!");
                })
                .catch((error) => {
                    console.error("Error during Data Source initialization", error);
                    throw error;
                });
        },
    },
    redis: {
        initialize: () => {
            // Redis initialization (if needed)
            getRedis();
        },
    },
    cache: {
        functions: [
            ensureCacheCountries,
            ensureCacheProjects,
            ensureCacheProgramTypes,
        ],
        interval: 1 * 60 * 1000, // Refresh every minute
    },
    middleware: [languageMiddleware],
    logger: {
        configure: (proxy: string, name: string) => {
            configureLogger(proxy, name);
        },
    },
    routesList: RoutesList,
    healthCheck: true,
    errorHandling: true,
    workers: [updateWorker, autosaveWorker],
});

export let app!: ReturnType<typeof server.getApp>;

server
    .start()
    .then(() => {
        app = server.getApp();
        console.log("Service started successfully");
    })
    .catch((error) => {
        console.error("Error during service start", error);
        process.exit(1);
    });

// Graceful shutdown
process.on('SIGTERM', async () => {
    console.log('SIGTERM received, shutting down gracefully');
    await server.stop();
    process.exit(0);
});

Configuration options

| Option | Type | Description | |--------|------|-------------| | hooks | IServerServiceHooks | Hooks for various initialization stages | | database | { initialize: () => Promise<void> } | Database initialization | | redis | { initialize: () => void } | Redis initialization | | cache | { functions: CacheFunction[], interval?: number } | Caching with automatic refresh | | portCheck | { enabled: boolean, check: (port) => Promise<void> } | Port check before startup | | middleware | RequestHandler[] | Custom Express middleware | | expressJson | { limit?: string } | JSON middleware settings | | expressUrlencoded | { limit?: string, extended?: boolean } | URL encoded middleware settings | | connectionPool | IConnectionPoolConfig | Connection pool settings | | dotenv | boolean | Load dotenv (default true) | | healthCheck | boolean | Add /health endpoint | | errorHandling | boolean | Global error handling | | serviceUrl | string | Custom service URL | | logger | { configure: (proxy, name) => void } | Logger configuration | | routesList | any | List of routes for registration in the proxy | | workers | Worker[] | Array of BullMQ workers | | workerMode | 'only-worker' \| 'only-service' \| 'both' | Startup mode (optional) |

Features

  • Automatic initialization - instanceAppV2() is called automatically
  • Flexible routes - support for arrays, Router, and Express App
  • Startup modes - automatic detection or explicit setting
  • Graceful shutdown - proper closing of workers and server
  • Caching - automatic refresh at an interval
  • Hooks - extension points at various stages
  • Health check - automatic /health endpoint
  • Error handling - global error handling

🚀 Development

Building the project

npm run build

Development mode

npm run dev

Production mode

npm run prod

Testing

npm test

Publishing

npm run publish

📝 License

This project is licensed under the ISC License - see the LICENSE file for details.


Made with ❤️ by Dev2Studio


🇺🇦 Українська версія | 🇬🇧 English Version