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

@ackplus/nest-crud-request

v2.0.1

Published

Framework-agnostic query builder for REST APIs - build complex queries with filtering, relations, and pagination for both frontend and backend

Readme

@ackplus/nest-crud-request

Part of the nest-crud family — one server + two query-builder clients, released together at the same version:

| Package | Get it | Docs | | --- | --- | --- | | @ackplus/nest-crud — NestJS + TypeORM server | 📦 npm | Getting started | | @ackplus/nest-crud-request — JS / TS query builder · this package | 📦 npm | Client guide | | nest_crud_request — Dart / Flutter query builder | 📦 pub.dev | Flutter guide |

📖 Full documentation: https://ack-solutions.github.io/nest-crud/ · 🧩 All packages · 💻 GitHub

Framework-agnostic query builder for REST APIs that follow the @ackplus/nest-crud request format. Write filters, relations, select, order, and pagination with a fluent, type-safe API — no string gymnastics, no duplicated logic between frontend and backend.

Works in React, Angular, Vue, Node.js, and any TypeScript environment. Zero runtime dependencies beyond tslib.


Table of contents

  1. Install
  2. Quick start
  3. QueryBuilder
  4. WhereBuilder
  5. Operators
  6. Order and pagination
  7. Relations
  8. Aggregates & HAVING
  9. Soft-delete flags
  10. Output: toObject() vs toObject(true)
  11. Custom keys (set)
  12. Recipes
  13. Types & enums reference
  14. Limits

Install

npm install @ackplus/nest-crud-request

No peer dependencies. Safe to use in browser bundles.


Quick start

import {
  QueryBuilder,
  WhereOperatorEnum,
  OrderDirectionEnum,
} from '@ackplus/nest-crud-request';

const params = new QueryBuilder()
  .where('isActive', WhereOperatorEnum.EQ, true)
  .addRelation('posts', ['id', 'title'])
  .addOrder('createdAt', OrderDirectionEnum.DESC)
  .setTake(10)
  .setSkip(0)
  .toObject();

const query = new URLSearchParams(params as Record<string, string>).toString();
const res = await fetch(`/users?${query}`);

That produces:

GET /users?where=%7B%22isActive%22%3A%7B%22%24eq%22%3Atrue%7D%7D
          &relations=%7B%22posts%22%3A%7B%22select%22%3A%5B%22id%22%2C%22title%22%5D%7D%7D
          &order=%7B%22createdAt%22%3A%22DESC%22%7D
          &take=10&skip=0

which is exactly what a @ackplus/nest-crud controller expects.


QueryBuilder

The top-level builder. Every method returns this, so chain freely.

Construction

new QueryBuilder();                        // empty
new QueryBuilder({ where: {...} });        // seed from options

Select (root columns)

qb.addSelect('id');
qb.addSelect(['email', 'firstName']);
qb.removeSelect('firstName');

Where

Four call shapes, all accepted by where / andWhere / orWhere:

// 1. field + value (EQ shorthand)
qb.where('status', 'active');

// 2. field + operator + value
qb.where('age', WhereOperatorEnum.GT_OR_EQ, 18);

// 3. raw object
qb.where({ role: { $in: ['admin', 'editor'] } });

// 4. nested callback — builds a grouped condition
qb.orWhere((b) => {
  b.where('role', 'admin');
  b.andWhere('verified', true);
});
  • where / andWhere — combined with AND
  • orWhere — combined with OR

Relations

qb.addRelation('posts');                                          // join, all columns
qb.addRelation('posts', ['id', 'title']);                         // pick columns
qb.addRelation('posts', ['id', 'title'], { published: { $eq: true } }); // + scoped where
qb.addRelation('posts', ['id'], undefined, 'inner');              // inner join
qb.addRelation('posts', { select: ['id'], joinType: 'inner' });   // object config
qb.removeRelation('posts');

joinType defaults to left. Use 'inner' to drop root rows that have no related row. The object-config form accepts { select?, where?, joinType? }.

Order

qb.addOrder('createdAt', OrderDirectionEnum.DESC);
qb.addOrder('email', OrderDirectionEnum.ASC);
qb.removeOrder('email');

addOrder also accepts an aggregate alias.

Aggregates & HAVING

Attach a per-row count / sum / avg / min / max over a relation, then optionally filter (having) and sort by the alias:

import { AggregateFnEnum, WhereOperatorEnum, OrderDirectionEnum } from '@ackplus/nest-crud-request';

qb.addAggregate({ fn: AggregateFnEnum.COUNT, field: 'posts.id', as: 'postCount' });
qb.having('postCount', WhereOperatorEnum.GT, 5);   // same call shapes as where()
qb.addOrder('postCount', OrderDirectionEnum.DESC);
qb.removeAggregate('postCount');                   // drop one by alias
  • having / andHaving / orHaving mirror where / andWhere / orWhere.
  • See the full Aggregates & HAVING section for the response shape.

Pagination

qb.setTake(20);
qb.setSkip(40);

No setPage / setPerPage helpers — compute them yourself: skip = (page - 1) * perPage.

Soft-delete

qb.setWithDeleted(true);  // include deleted rows
qb.setOnlyDeleted(true);  // only deleted rows

Options merging

qb.setOptions({ where: {...}, take: 50 });      // replace everything
qb.mergeOptions({ where: {...} });              // shallow merge (default)
qb.mergeOptions({ where: {...} }, true);        // deep merge

Output

qb.toObject();       // default — where/relations/select/order JSON-stringified
qb.toObject(true);   // nested — everything stays as native objects
qb.toJson();         // JSON string of the nested form (equivalent to JSON.stringify(toObject(true)))

See Output.


WhereBuilder

WhereBuilder builds a where object on its own — handy if you want to compose filter logic separately from the full query.

import { WhereBuilder, WhereOperatorEnum } from '@ackplus/nest-crud-request';

const where = new WhereBuilder()
  .where('age', WhereOperatorEnum.GT, 18)
  .orWhere((b) => {
    b.where('role', 'admin');
    b.andWhere('department', 'IT');
  })
  .toObject();

// { $or: [ { age: { $gt: 18 } }, { role: { $eq: 'admin' }, department: { $eq: 'IT' } } ] }

Methods

| Method | Purpose | | --- | --- | | where(...) / andWhere(...) / orWhere(...) | Same four call shapes as QueryBuilder.where | | removeWhere(field) | Drop a field from the current conditions | | clear() | Reset to empty | | hasConditions() | true if anything has been added | | toObject() | Raw where object (never stringified) | | toJson() | JSON.stringify(toObject()) |

Feed the result into a QueryBuilder:

qb.setOptions({ where: where.toObject() });
// or
qb.where(where.toObject());

WhereBuilder is usable entirely standalone; you don't need a QueryBuilder at all.


Operators

Import them from the enum or pass the raw $... string — both work.

| Enum | Value | Meaning | | --- | --- | --- | | EQ | $eq | Equal (default when you pass a scalar without an operator) | | NOT_EQ | $ne | Not equal | | IEQ | $ieq | Case-insensitive equal | | GT / GT_OR_EQ | $gt / $gte | Greater than / or equal | | LT / LT_OR_EQ | $lt / $lte | Less than / or equal | | IN / NOT_IN | $in / $notIn | In / not in array | | LIKE / NOT_LIKE | $like / $notLike | SQL LIKE / NOT LIKE | | ILIKE / NOT_ILIKE | $iLike / $notIlike | Case-insensitive LIKE / NOT LIKE | | STARTS_WITH / ENDS_WITH | $startsWith / $endsWith | Prefix / suffix | | ISTARTS_WITH / IENDS_WITH | $iStartsWith / $iEndsWith | Case-insensitive prefix / suffix | | IN_L / NOT_IN_L | $inL / $notinL | Case-insensitive IN / NOT IN | | CONT_ARR | $contArr | Postgres array contains (@>) | | INTERSECTS_ARR | $intersectsArr | Postgres array intersects (&&) | | IS_NULL / IS_NOT_NULL | $isNull / $isNotNull | IS NULL / IS NOT NULL (no value) | | BETWEEN / NOT_BETWEEN | $between / $notBetween | Range [start, end] | | IS_TRUE / IS_FALSE | $isTrue / $isFalse | Boolean truthiness | | EXISTS / NOT_EXISTS | $exists / $notExists | Relation has / has no rows (key is a relation) |

Logical operators (WhereLogicalOperatorEnum): AND ($and), OR ($or).

// value-less operators — pass the operator alone (2-arg form)
qb.where('deletedAt', WhereOperatorEnum.IS_NULL);   // { deletedAt: { $isNull: true } }
// relation existence — the key is a relation name
qb.where('posts', WhereOperatorEnum.EXISTS, true);  // { posts: { $exists: true } }

$exists / $notExists test only whether a relation has rows (a subquery on the server) — they don't join it. To also return the related rows, add the relation with addRelation.


Order and pagination

new QueryBuilder()
  .addOrder('createdAt', OrderDirectionEnum.DESC)
  .addOrder('name', OrderDirectionEnum.ASC)
  .setTake(20)
  .setSkip(40)
  .toObject();

// { order: '{"createdAt":"DESC","name":"ASC"}', take: 20, skip: 40 }

Relations

new QueryBuilder()
  .addRelation('posts', ['id', 'title'], { published: { $eq: true } })
  .addRelation('profile')
  .toObject(true);

// {
//   relations: {
//     posts: { select: ['id', 'title'], where: { published: { $eq: true } } },
//     profile: true,
//   }
// }

joinType (default left) is supported directly — positionally or via the object-config form:

new QueryBuilder()
  .addRelation('posts', ['id'], undefined, 'inner')
  .addRelation('profile', { joinType: 'inner' })
  .toObject(true);

// { relations: { posts: { select: ['id'], joinType: 'inner' }, profile: { joinType: 'inner' } } }

RelationBuilder is also exported if you want to compose relations separately:

import { RelationBuilder } from '@ackplus/nest-crud-request';

const relations = new RelationBuilder()
  .add('posts', { select: ['id', 'title'], where: { status: 'published' } })
  .toObject();

Aggregates & HAVING

Attach computed values over a relation to each returned row, filter them with having, and sort by their alias.

import {
  QueryBuilder, AggregateFnEnum, WhereOperatorEnum, OrderDirectionEnum,
} from '@ackplus/nest-crud-request';

const params = new QueryBuilder()
  .addAggregate({ fn: AggregateFnEnum.COUNT, field: 'posts.id', as: 'postCount' })
  .addAggregate({ fn: AggregateFnEnum.SUM, field: 'posts.likes', as: 'totalLikes' })
  .having('postCount', WhereOperatorEnum.GT, 5)
  .addOrder('postCount', OrderDirectionEnum.DESC)
  .toObject();

// the server returns each row with the aggregate attached:
// { items: [ { id, name, postCount, totalLikes } ], total }

| fn (AggregateFnEnum) | Result | Empty relation | | --- | --- | --- | | COUNT ('count') | number of related rows | 0 | | SUM ('sum') | sum of field | 0 | | AVG ('avg') | average of field | null | | MIN / MAX ('min' / 'max') | min / max of field | null |

interface AggregateSpec {
  fn: AggregateFnEnum | 'count' | 'sum' | 'avg' | 'min' | 'max';
  field: string;       // relation-qualified, e.g. 'posts.id'
  as: string;          // alias; used by having() and addOrder()
  distinct?: boolean;  // COUNT(DISTINCT …)
  where?: WhereOptions; // filter the related rows — same operators as where
}
  • where on an aggregate filters only the related rows it counts/sums (e.g. count only published posts): addAggregate({ fn: 'count', field: 'posts.id', as: 'published', where: { status: 'published' } }).
  • having(...) / andHaving(...) / orHaving(...) take the same call shapes as where(...), but the key is an aggregate alias (e.g. postCount).
  • removeAggregate(alias) drops one.
  • In toObject() aggregates/having are JSON-stringified; in toObject(true) they stay as native array/object.

Soft-delete flags

qb.setWithDeleted(true);   // ?withDeleted=true
qb.setOnlyDeleted(true);   // ?onlyDeleted=true

Both flags are forwarded as-is. The server decides how to interpret them (see @ackplus/nest-crud).


Output: toObject() vs toObject(true)

toObject() is designed to be dropped straight into URLSearchParams or axios({ params }). It JSON-stringifies the complex fields (where, relations, select, order) because they don't serialize cleanly otherwise.

new QueryBuilder()
  .where('isActive', true)
  .addOrder('createdAt', OrderDirectionEnum.DESC)
  .toObject();
// {
//   where: '{"isActive":{"$eq":true}}',
//   order: '{"createdAt":"DESC"}'
// }

toObject(true) (or toJson()) keeps everything as native values — useful for logging, diffing, storing in URL state, or passing directly to a backend Node client.

new QueryBuilder()
  .where('isActive', true)
  .addOrder('createdAt', OrderDirectionEnum.DESC)
  .toObject(true);
// {
//   where: { isActive: { $eq: true } },
//   order: { createdAt: 'DESC' }
// }

Empty sections are dropped from both forms.


Custom keys (set)

Anything not in the standard schema goes through set:

qb.set('search', 'alice');
qb.set('tenantId', 'acme');
qb.set('relations', { posts: { joinType: 'inner' } });

set is both an extensibility hook and an escape hatch for the less-common relation / where shapes.


Recipes

Pagination

function buildPage(page: number, perPage: number) {
  return new QueryBuilder()
    .setTake(perPage)
    .setSkip((page - 1) * perPage);
}

Full-text-ish search

function buildSearch(term: string) {
  return new QueryBuilder().where((b) => {
    b.where('firstName', WhereOperatorEnum.ILIKE, `%${term}%`);
    b.orWhere('lastName', WhereOperatorEnum.ILIKE, `%${term}%`);
    b.orWhere('email', WhereOperatorEnum.ILIKE, `%${term}%`);
  });
}

Axios client

import axios from 'axios';

async function listUsers(qb: QueryBuilder) {
  const { data } = await axios.get('/users', { params: qb.toObject() });
  return data; // { items, total }
}

Deserializing back into a QueryBuilder

const stored = qb.toObject(true); // native shape
const restored = new QueryBuilder(stored);

Types & enums reference

Everything below is exported from the package root.

Public exports

  • Classes: QueryBuilder, WhereBuilder, RelationBuilder
  • Enums: WhereOperatorEnum, WhereLogicalOperatorEnum, OrderDirectionEnum, AggregateFnEnum
  • Types: QueryBuilderOptions, AggregateSpec, WhereObject, WhereOptions, RelationObject, RelationObjectValue, RelationOptions, FindManyResponse<T>, FindAllResponse<T>, WhereBuilderCondition

Shapes

interface QueryBuilderOptions {
  select?: string[] | string;
  relations?: RelationOptions | string;
  where?: WhereOptions | string;
  order?: Record<string, OrderDirectionEnum> | string;
  aggregates?: AggregateSpec[] | string;
  having?: WhereOptions | string;
  skip?: number;
  take?: number;
  withDeleted?: boolean;
  onlyDeleted?: boolean;
  [extra: string]: any;
}

enum AggregateFnEnum { COUNT = 'count', SUM = 'sum', AVG = 'avg', MIN = 'min', MAX = 'max' }

interface AggregateSpec {
  fn: AggregateFnEnum | 'count' | 'sum' | 'avg' | 'min' | 'max';
  field: string;        // relation-qualified path, e.g. 'posts.id'
  as: string;           // alias used by having() / addOrder()
  distinct?: boolean;
  where?: WhereOptions; // filter the related rows (same operators as where)
}

type RelationObjectValue = {
  select?: string[];
  where?: WhereObject | WhereObject[];
  joinType?: 'left' | 'inner';
};

type RelationObject = Record<string, RelationObjectValue | boolean>;
type RelationOptions = string | string[] | RelationObject;

type WhereObject = {
  [key: string]: any;
  $and?: WhereObject | WhereObject[];
  $or?: WhereObject | WhereObject[];
};
type WhereOptions = WhereObject | WhereObject[];

type WhereBuilderCondition =
  | [string, any]                               // field, value  -> $eq
  | [string, WhereOperatorEnum, any]            // field, op, value
  | [Record<string, any>]                       // raw object
  | [(builder: WhereBuilder) => void];          // nested callback

Limits

  • No setPage / setPerPage helpers (compute skip manually).
  • No built-in query-string serializer — pipe toObject() through URLSearchParams, qs, or your HTTP client's params option.
  • Doesn't validate server-side operators — you can build filters the server rejects if you mix operators and column types carelessly.

For the matching backend, see @ackplus/nest-crud.


Related packages

| Package | Registry | For | | --- | --- | --- | | @ackplus/nest-crud | npm | The NestJS + TypeORM server | | nest_crud_request | pub.dev | The Dart/Flutter twin of this builder |

This package and its Dart twin produce an identical wire format and publish together at one version. Full docs: https://ack-solutions.github.io/nest-crud/ · all packages.


License

MIT © Ackplus