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

nest-utilities

v8.7.1

Published

Extends Nestjs projects using Fundering with endpoint helpers

Readme

Description

When building NestJS APIs you usually find yourself writing the same CRUD plumbing for every module. nest-utilities removes that boilerplate by giving you:

  • An abstract CrudController that wraps a fundering CrudService and exposes ready-to-go create / read / update / delete handlers.
  • A @QueryOptions() parameter decorator that turns HTTP query strings (or a JSON header) into Mongoose-friendly options — including filtering, sorting, pagination, projection, population and aggregation $addFields.
  • An HttpExceptionFilter that produces consistent error payloads.
  • A handful of small interfaces, constants and utilities that make the above safe and predictable.

The package is intentionally thin: persistence, hooks and authorisation logic live in fundering; nest-utilities is the HTTP-side adapter on top of it.

Table of contents

Installation

npm install nest-utilities fundering

nest-utilities ships TypeScript types out of the box. The peer dependencies are:

| Package | Version | | ---------------- | ---------- | | @nestjs/common | >= 8.0.0 | | @nestjs/core | >= 8.0.0 | | fundering | >= 1.3.0 | | mongoose | >= 5.12.0| | rxjs | >= 7.1.0 |

Make sure those are already installed in your NestJS project.

Quick start

The example below wires up a fully functional /users resource with filtering, sorting, pagination and population, while still letting you customise individual endpoints when needed.

// user.schema.ts
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Document } from "mongoose";

@Schema()
export class User extends Document {
  @Prop() firstName: string;
  @Prop() lastName: string;
  @Prop() email: string;
}

export const UserSchema = SchemaFactory.createForClass(User);
// user.service.ts
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { CrudService } from "fundering";
import { Model } from "mongoose";
import { User } from "./user.schema";

@Injectable()
export class UserService extends CrudService<User> {
  constructor(@InjectModel(User.name) userModel: Model<User>) {
    super(userModel);
  }
}
// user.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
  Put,
} from "@nestjs/common";
import {
  CrudController,
  IExpressQueryOptions,
  QueryOptions,
} from "nest-utilities";
import { User } from "./user.schema";
import { UserService } from "./user.service";

@Controller("users")
export class UserController extends CrudController<User> {
  constructor(userService: UserService) {
    super(userService);
  }

  @Post()
  create(@Body() user: User, @QueryOptions() options: IExpressQueryOptions) {
    return this.handleCreate(user, options);
  }

  @Get()
  find(@QueryOptions() options: IExpressQueryOptions) {
    return this.handleFind(options.match ?? {}, options);
  }

  @Get(":id")
  findById(
    @Param("id") id: string,
    @QueryOptions() options: IExpressQueryOptions,
  ) {
    return this.handleFindById(id, options);
  }

  @Put(":id")
  put(
    @Param("id") id: string,
    @Body() user: User,
    @QueryOptions() options: IExpressQueryOptions,
  ) {
    return this.handlePut(user, id, options);
  }

  @Patch(":id")
  patch(
    @Param("id") id: string,
    @Body() user: Partial<User>,
    @QueryOptions() options: IExpressQueryOptions,
  ) {
    return this.handlePatch(user, id, options);
  }

  @Delete(":id")
  remove(
    @Param("id") id: string,
    @QueryOptions() options: IExpressQueryOptions,
  ) {
    return this.handleDelete(id, options);
  }
}

That's it. The endpoints now accept the query parameters listed below, automatically expose X-Total-Count when asked for, and route every error through Nest's standard exception flow.

API reference

CrudController<ModelType>

Abstract class you extend in your own controller. It receives a fundering CrudService in its constructor and exposes the following handlers — none of them register routes for you, you wire them up with the usual Nest decorators (@Get, @Post, etc.). All handlers accept an IExpressQueryOptions argument so authorisation hooks inside fundering get access to the original Request and Response.

| Handler | Signature | Notes | | ------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | handleCreate | (model, options?) => Promise<ModelType> | Wraps crudService.create. | | handleCreateMany | (models, options?) => Promise<ModelType[]> | Wraps crudService.createMany. | | handleFind | (conditions, options?) => Promise<ModelType[]> | If options.includeCount is true, calls crudService.count first so the X-Total-Count header gets populated. | | handleFindById | (id, options?) => Promise<ModelType> | Throws NotFoundException("No model found") if the document is not visible to the requester. | | handlePut | (model, id?, options?) => Promise<ModelType> | Validates visibility via handleFindById before replacing the document. | | handlePutMany | (models, options?) => Promise<ModelType[]> | Validates visibility for each model, then replaces them in bulk. | | handlePatch | (partial, id?, options?) => Promise<ModelType> | Validates visibility, then merges (crudService.mergeModel). | | handlePatchMany | (partials, options?) => Promise<ModelType[]> | Validates visibility for each, then bulk merges. | | handleDelete | (id, options?) => Promise<ModelType \| null> | Validates visibility, then removes via crudService.deleteById. |

When the constructor runs, an addCountHeaderHook is attached to the underlying service. It hooks into fundering's postCount lifecycle and sets two response headers when a count is performed:

  • X-Total-Count: <number>
  • Access-Control-Expose-Headers: X-Total-Count, ... (existing values are preserved)

@QueryOptions()

A NestJS parameter decorator that converts the incoming HTTP request into an IExpressQueryOptions object. Two transport modes are supported:

  1. Query string — the default. Use bracket notation for nested keys, e.g. ?match[firstName]=John&limit=10.
  2. Header — set x-query-options to a JSON-encoded IHttpOptions object. Useful when you would exceed URL length limits or want to send deeply nested filters cleanly.

The decorator accepts an optional IQueryOptionsConfig:

@Get()
find(
  @QueryOptions({ maxDepth: 5 }) options: IExpressQueryOptions,
) { ... }

maxDepth (default 3) caps how deeply the parser will recurse into match filters and populate paths to keep accidental N+1 explosions in check.

The returned object is the parsed IQueryOptions plus the original request and response, so authorisation hooks downstream in fundering can inspect them.

HttpExceptionFilter

A @Catch()-all filter that normalises errors into JSON. Pass showErrorDetails in the constructor to control whether stack traces and original messages leak to the client (typical pattern: process.env.NODE_ENV !== "production").

// main.ts
import { HttpExceptionFilter } from "nest-utilities";

const app = await NestFactory.create(AppModule);
app.useGlobalFilters(
  new HttpExceptionFilter(process.env.NODE_ENV !== "production"),
);

Behaviour:

  • Instances of HttpException are forwarded with their original status and body.
  • Anything else becomes { status: 500, message: "Internal server error" }. When showErrorDetails is true, the response also contains message (the original error message) and stack.

Interfaces

// re-exported from the package root
import {
  IModel,
  IExpressQueryOptions,
  IHttpOptions,
  IQueryOptionsConfig,
} from "nest-utilities";
  • IModel{ id?: any; _id?: any }. The minimum shape CrudController requires from your model type.
  • IExpressQueryOptions — extends fundering's IQueryOptions with request, response and includeCount. This is what @QueryOptions() injects.
  • IHttpOptions — describes the raw HTTP query string shape (the thing the user sends; see Query parameters).
  • IQueryOptionsConfig{ maxDepth?: number }, accepted by @QueryOptions().

Custom operators

nest-utilities exposes one custom MongoDB-style operator out of the box:

  • $isNull?match[deletedAt][$isNull]=true becomes { deletedAt: { $eq: null } }. Truthy values (true, "true", 1, "1") check for null; everything else inverts to { $ne: null }.

The operator table is exported as customOperators from the package root if you want to extend it in your own utilities.

Utilities

  • queryToOptions(query, config?) — the parsing function used internally by @QueryOptions(). Useful in tests or when you want the same parsing behaviour outside of a request context.
  • addCountHeaderHook(service) — attaches the X-Total-Count response hook to a CrudService. CrudController calls this for you, but you can call it directly if you bypass the controller abstraction.

Query parameters

Every handler that goes through @QueryOptions() accepts the parameters below. They map to stages of the MongoDB aggregation pipeline.

| Param | Type | Description | | -------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------- | | match | Record<string, any> | $match filter. Supports MongoDB operators ($in, $gt, $regex, …) and the $isNull extension. | | sort | string[] | $sort. Prefix a field with - for descending: ?sort=-createdAt&sort=lastName. | | skip | number | $skip. Use for pagination. | | offset | number (deprecated) | Alias for skip. Will be removed in a future major. | | limit | number | $limit. | | select | string[] | $project. Dot notation supported: ?select=name&select=address.city. | | populate | (string \| IPopulateOptions)[] | Mongoose-style populate. Strings allow dot notation; objects allow match/sort/limit per join. | | addFields | Record<string, any> | $addFields stage; usable in subsequent match/sort. | | distinct | string \| string[] | Returns unique values for the given field(s). | | random | boolean | When truthy, returns documents in random order (overrules sort). | | includeCount | boolean | When true, the response sets X-Total-Count to the unpaginated match count. | | custom | Record<string, any> | Free-form bag forwarded to fundering hooks for application-specific behaviour. |

Primitive coercion. By default every query string value is a string. Wrap a value in {{ }} to coerce it: {{42}}, {{true}}, {{null}}, {{undefined}}. This is the canonical way to do ?match[isPublished]={{true}} instead of receiving the literal string "true".

Depth limits. match keys deeper than maxDepth (default 3) are silently dropped, and populate paths are truncated. Bump maxDepth per-decorator if you need more.

Header transport. For complex queries, send a single x-query-options header containing a JSON-stringified IHttpOptions:

GET /users
x-query-options: {"match":{"firstName":{"$in":["John","Jane"]}},"limit":10}

Recipes

Pagination with total count

Tell the server you want the count, then read it from the response header on the client.

GET /users?limit=20&skip=40&includeCount=true

The response includes X-Total-Count: 137. The header is also added to Access-Control-Expose-Headers so browsers can read it cross-origin.

Filtering and sorting

GET /users?match[firstName][$regex]=^Jo&sort=-createdAt&sort=lastName

Populating relationships with conditions

Use the header transport for nested options:

{
  "populate": [
    {
      "path": "school",
      "match": { "active": "{{true}}" },
      "populate": [{ "path": "students", "limit": 5 }]
    }
  ]
}

Selecting a subset of fields

GET /users?select=firstName&select=lastName&select=address.city

Distinct values

GET /users?distinct=email

Custom-operator null check

GET /users?match[deletedAt][$isNull]=true

Random sample

GET /quotes?limit=5&random=true

Per-controller maxDepth override

@Get()
find(
  @QueryOptions({ maxDepth: 6 }) options: IExpressQueryOptions,
) {
  return this.handleFind(options.match ?? {}, options);
}

Centralised error responses

// main.ts
app.useGlobalFilters(
  new HttpExceptionFilter(process.env.NODE_ENV !== "production"),
);

In production, all unhandled errors are masked behind a 500 Internal server error. In development you get the original message and stack to debug with.

Bypassing the controller for service calls

addCountHeaderHook works on any CrudService instance, so you can keep using the same response-header behaviour even when you call services directly from a non-CRUD controller:

import { addCountHeaderHook } from "nest-utilities";

constructor(private userService: UserService) {
  addCountHeaderHook(this.userService);
}

Compatibility

Built and tested against:

  • NestJS >= 8
  • Mongoose >= 5.12
  • Fundering >= 1.3
  • RxJS >= 7.1

The package is published as CommonJS (distribution/index.js) with bundled type declarations (distribution/index.d.ts).

License

ISC © MartinDrost

For deeper guides and migration notes, see the wiki and the issue tracker.