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

@tacxou/nestjs_module_restools

v1.1.1

Published

Simple tools for NestJS framework and REST APIs

Readme

NestJS Restools Module

Utilities for NestJS REST APIs: abstract base classes, validation pipes, query-string filters, request context, and optional Mongoose CRUD helpers.

Overview

The package is split into a core entry point and optional submodules. Optional modules are not re-exported from the main entry so consumers are not forced to install mongoose or other peer dependencies.

| Module | Source folder | Import path | Extra dependencies | |--------|---------------|-------------|-------------------| | Core (abstracts, pipes, cluster) | src/_abstracts, _pipes, _services | @tacxou/nestjs_module_restools | @nestjs/common, @nestjs/core | | Request context | src/request-context | @tacxou/nestjs_module_restools/dist/request-context | — | | Search filter schema | src/search-filter-schema | @tacxou/nestjs_module_restools/dist/search-filter-schema | mongoose (for ObjectId filters) | | Mongoose | src/mongoose | @tacxou/nestjs_module_restools/dist/mongoose | mongoose, @nestjs/mongoose, @nestjs/event-emitter | | Real IP decorator | src/real-ip | @tacxou/nestjs_module_restools/dist/real-ip | request-ip |

Internal utilities under src/_utils (e.g. memoize.util.ts) are not published.


Installation

Core

yarn add @tacxou/nestjs_module_restools
# peers: @nestjs/common, @nestjs/core

Optional modules

# Request context (AsyncLocalStorage for req/res in services)
# No extra packages beyond NestJS

# Search filters (query string → MongoDB-style filters)
yarn add mongoose

# Mongoose CRUD service + validation
yarn add mongoose @nestjs/mongoose @nestjs/event-emitter

# RealIp decorator (client IP from request headers)
yarn add request-ip
# import from @tacxou/nestjs_module_restools/dist/real-ip

After yarn build, optional modules are consumed from dist/<module> (see import examples below).


Core modules

Import from the package root:

import {
  AbstractService,
  AbstractController,
  DtoValidationPipe,
  AppClusterService,
} from '@tacxou/nestjs_module_restools'

_abstractsAbstractService & AbstractController

Base classes with logging and naming helpers.

AbstractService

  • logger — Nest Logger named after the service.
  • request — HTTP request from constructor context or RequestContextStorage (if request-context module is used).
  • eventEmitter — optional EventEmitter2 from context.
  • moduleName — derived from request.path (first segment, capitalized) or context.moduleName.
  • serviceName — class name without Service suffix, or context.serviceName.

AbstractController

  • logger, moduleRef, controllerName (same naming pattern as services).

Context injection

import { Injectable } from '@nestjs/common'
import { ModuleRef } from '@nestjs/core'
import { EventEmitter2 } from '@nestjs/event-emitter'
import { AbstractService, AbstractServiceContext } from '@tacxou/nestjs_module_restools'

@Injectable()
export class OrderService extends AbstractService {
  constructor(moduleRef: ModuleRef, eventEmitter: EventEmitter2) {
    super({
      moduleRef,
      eventEmitter,
      moduleName: 'Orders',   // optional override
      serviceName: 'Order',   // optional override
    } as AbstractServiceContext)
  }

  doWork() {
    const user = this.request?.user
    this.logger.log(`Handling order for ${user?.username ?? 'anonymous'}`)
  }
}

Pair with RequestContextModule so this.request is available without passing req manually.


_pipesDtoValidationPipe

Extends Nest ValidationPipe with:

  • transform: true and implicit conversion enabled by default.
  • Flat validation errors: { "field": "message", "parent.child": "message" }.
  • Custom exceptionFactory returning { statusCode: 400, message, validations }.

Global registration

import { Module } from '@nestjs/common'
import { APP_PIPE } from '@nestjs/core'
import { DtoValidationPipe } from '@tacxou/nestjs_module_restools'

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: DtoValidationPipe,
    },
  ],
})
export class AppModule {}

Per-controller

@UsePipes(DtoValidationPipe)
@Controller('users')
export class UsersController {}

_servicesAppClusterService

Runs the Nest bootstrap callback in Node cluster mode (one worker per CPU by default).

import { NestFactory } from '@nestjs/core'
import { AppClusterService } from '@tacxou/nestjs_module_restools'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  await app.listen(3000)
}

AppClusterService.clusterize(bootstrap, {
  clusterize: process.env.CLUSTERIZE === 'true',
  forks: 4,
  name: 'MyApi',
})

| Option | Default | Description | |--------|---------|-------------| | clusterize | — | If falsy, runs callback once (no clustering). | | forks | os.cpus().length | Number of worker processes. | | name | AppClusterService | Log label. |


Optional modules

Import from dist/<module> after building or from the published package layout:

import { RequestContextModule } from '@tacxou/nestjs_module_restools/dist/request-context'
import { RealIp } from '@tacxou/nestjs_module_restools/dist/real-ip'

real-ip

Parameter decorator that resolves the client IP via request-ip.

yarn add request-ip
import { Controller, Get } from '@nestjs/common'
import { RealIp } from '@tacxou/nestjs_module_restools/dist/real-ip'

@Controller('auth')
export class AuthController {
  @Get('ping')
  ping(@RealIp() ip: string | null) {
    return { ip }
  }
}

request-context

Stores the current Express req / res in AsyncLocalStorage so any code in the same async chain can read them (e.g. AbstractService.request).

Setup

import { Module } from '@nestjs/common'
import { RequestContextModule } from '@tacxou/nestjs_module_restools/dist/request-context'

@Module({
  imports: [RequestContextModule],
})
export class AppModule {}

RequestContextModule applies RequestContextMiddleware to all routes (*).

Read context in a service

import { RequestContextStorage } from '@tacxou/nestjs_module_restools/dist/request-context'

const ctx = RequestContextStorage.currentContext
const req = ctx?.req

Flow: HTTP request → middleware → storage.run(context, next) → controllers / services see currentContext.


search-filter-schema

Parses query-string filters and pagination into objects usable with MongoDB (or any store).

Import:

import {
  SearchFilterSchema,
  SearchFilterOptions,
  filterSchema,
  filterOptions,
} from '@tacxou/nestjs_module_restools/dist/search-filter-schema'

@SearchFilterSchema() / filterSchema()

Syntax: filters[PREFIX + FIELD]=VALUE

| Prefix | Description | |--------|-------------| | : | Equal | | # | Number equal | | !# | Number not equal | | !: | Not equal | | > | Greater than | | >| | Greater than or equal | | < | Less than | | <| | Less than or equal | | @ | In | | !@ | Not in | | @# | Number in | | !@# | Number not in | | ^ | Regex | | ? | Boolean (true / false / 1 / 0) |

Examples

  • filters[=subject]=53{ subject: 53 } (or string, depending on field)
  • filters[:concernedTo]=65fab2d6946a5ede152f2689 → ObjectId when convertObjectId is enabled
  • filters[^sequence]=/53/ → regex on sequence

Controller usage

import { Controller, Get } from '@nestjs/common'
import {
  SearchFilterSchema,
  SearchFilterOptions,
} from '@tacxou/nestjs_module_restools/dist/search-filter-schema'

@Controller('search')
export class SearchController {
  @Get()
  search(
    @SearchFilterSchema() filters: Record<string, unknown>,
    @SearchFilterOptions() options: { limit: number; skip: number; sort: Record<string, 1 | -1> },
  ) {
    return { filters, options }
  }
}

curl example

curl --request GET \
  --url 'http://localhost/search?limit=9999&filters%5B%5Esequence%5D=%2F53%2F&sort%5Bmetadata.createdAt%5D=-1&sort%5Bsubject%5D=1'

# limit=9999
# filters[^sequence]=/53/
# sort[metadata.createdAt]=-1
# sort[subject]=1

Decorator options (see source): queryKey, strict, unsafe, convertObjectId, convertNull, allowedFilters, etc.

@SearchFilterOptions() / filterOptions()

Reads pagination and sort from the query string:

| Query key | Default | Role | |-----------|---------|------| | limit | 10 | Page size (allowUnlimited for no cap) | | skip / page | 0 | Offset (if both skip and page are set, skip is ignored) | | sort[field] | {} | 1 / -1 or asc / desc |


mongoose

Typed CRUD base service, route pipes, and exception filter for Mongoose 9.

Import:

import {
  AbstractServiceSchema,
  ServiceSchemaInterface,
  EventEmitterSeparator,
  ObjectIdValidationPipe,
  MongooseValidationFilter,
} from '@tacxou/nestjs_module_restools/dist/mongoose'

Dependencies: mongoose, @nestjs/mongoose, @nestjs/event-emitter, and core AbstractService (via extends).

AbstractServiceSchema<TRawDoc, THydrated>

Generic class-level types (no per-method <T> shadowing):

  • TRawDoc — plain document interface (_id?: Types.ObjectId, fields…).
  • THydrated — defaults to HydratedDocument<TRawDoc>.

CRUD methods (return hydrated documents, not Mongoose Query):

| Method | Returns | |--------|---------| | find(filter?, projection?, options?) | Promise<THydrated[]> | | findOne(filter?, …) | Promise<THydrated> (throws NotFoundException) | | findById(id, …) | Promise<THydrated> | | count(filter?, options?) | Promise<number> | | findAndCount(filter?, …) | Promise<[THydrated[], number]> | | create(data?, options?) | Promise<THydrated> | | update(id, update, options?) | Promise<THydrated> | | upsert(filter, update, options?) | Promise<THydrated> | | delete(id, options?) | Promise<THydrated> |

Example service

import { Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { EventEmitter2 } from '@nestjs/event-emitter'
import { ModuleRef } from '@nestjs/core'
import { HydratedDocument, Model, Types } from 'mongoose'
import { AbstractServiceSchema } from '@tacxou/nestjs_module_restools/dist/mongoose'

interface IUser {
  _id?: Types.ObjectId
  email: string
  name: string
}

type HydratedUser = HydratedDocument<IUser>

@Injectable()
export class UserService extends AbstractServiceSchema<IUser, HydratedUser> {
  protected readonly model: Model<IUser, {}, {}, {}, HydratedUser>

  constructor(
    @InjectModel('User') model: Model<IUser, {}, {}, {}, HydratedUser>,
    moduleRef: ModuleRef,
    eventEmitter: EventEmitter2,
  ) {
    super({ moduleRef, eventEmitter })
    this.model = model
  }
}

Use RequestContextModule so this.request and this.moduleName work inside hooks without passing req in the constructor.

EventEmitter hooks

When eventEmitter is set on the service context, each operation emits async hooks:

Event name pattern

{moduleName}.{serviceName}.service.{before|after}{Operation}

Example: users.user.service.beforeFind (segments lowercased; separator is .EventEmitterSeparator).

Before hooks — listeners can return partial overrides merged into the operation:

| Key | Effect | |-----|--------| | filter | Merged into query filter | | projection / options | Merged into Mongoose options | | data | Merged into create payload | | update | Merged into update query | | _id | Overrides id for findById / update / delete | | stop | If set, thrown immediately (abort operation) |

After hooks — listeners can return data, created, updated, deleted, result, or adjust count on findAndCount.

import { OnEvent } from '@nestjs/event-emitter'

@Injectable()
export class UserHooksListener {
  @OnEvent('users.user.service.beforeFind')
  beforeFind(payload: { filter?: Record<string, unknown>; eventName: string }) {
    return { filter: { deleted: { $ne: true } } }
  }
}

ObjectIdValidationPipe

Validates route/param strings and returns Types.ObjectId.

import { Controller, Get, Param } from '@nestjs/common'
import { Types } from 'mongoose'
import { ObjectIdValidationPipe } from '@tacxou/nestjs_module_restools/dist/mongoose'

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id', ObjectIdValidationPipe) id: Types.ObjectId) {
    return { id: id.toString() }
  }
}

MongooseValidationFilter()

Factory for an exception filter that maps Mongoose ValidationError and CastError to HTTP 406 with a validations map.

import { APP_FILTER } from '@nestjs/core'
import { MongooseValidationFilter } from '@tacxou/nestjs_module_restools/dist/mongoose'

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: MongooseValidationFilter(),
    },
  ],
})
export class AppModule {}

Pass custom exception types: MongooseValidationFilter([MyCustomError]).

Debug: add ?debug=1 to the query in non-production to include _exception in the response body.


Development

From the repository root:

yarn install
yarn build      # compile to dist/
yarn test       # Jest (*.spec.ts)
yarn test:types # TypeScript checks for mongoose typetest
yarn test:cov   # coverage

Source layout mirrors dist/:

src/
  index.ts                 # core exports only
  _abstracts/
  _pipes/
  _services/
  real-ip/
  request-context/
  search-filter-schema/
  mongoose/

License

MIT — see LICENSE.