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

laravel-query-builder-js

v1.0.2

Published

A fluent, framework-agnostic, Laravel-style query builder for JavaScript & TypeScript. Compose select / filter / sort / include / pagination and serialise to API query params. Works in React, Vue, Angular, and Node.

Readme

laravel-query-builder-js

npm version CI license types

A fluent, framework-agnostic, Laravel-style query builder for JavaScript & TypeScript.

Compose select, filter, sort, include (eager-loaded relations) and pagination on the client, then serialise it into query params (or a query string) for your API. It has no framework dependencies, so the exact same code works in React, Vue, Angular, and Node.js.

const query = new QueryBuilder()
  .where("state", "=", "Open")
  .whereIn("type", ["Buyer", "Tenant"])
  .sort({ createdAt: "desc" })
  .paginate(1, 25);

fetch("/api/leads?" + query.toQueryParams());

💡 Pairs perfectly with a Laravel backend. The query params this package produces are fully compatible with qubuilder — a PHP/Laravel package that turns the same select / filter / sort / include / pagination payload into a fully-chained Eloquent query. Build the query on the client here, consume it on the server there. See Laravel backend (qubuilder).


Table of contents


Features

  • 🔗 Fluent, chainable API — reads like a sentence.
  • 🧩 Nested relations — eager-load related resources with their own filters/sorts.
  • 🪺 Grouped conditions — build (a OR b) AND c with callbacks.
  • 🐍 Automatic snake_case — select fields and sort keys are snake_cased for the backend.
  • 📦 ESM + CJS + types — first-class TypeScript, works everywhere.
  • 🪶 Tiny footprint — one runtime dependency (lodash).
  • 🧪 Well tested — full unit-test suite.

Installation

npm install laravel-query-builder-js
# or
yarn add laravel-query-builder-js
# or
pnpm add laravel-query-builder-js

Quick start

import { QueryBuilder } from "laravel-query-builder-js";

const query = new QueryBuilder()
  .select(["id", "firstName", "email"])
  .where("state", "=", "Open")
  .whereIn("type", ["Buyer", "Tenant"])
  .sort({ createdAt: "desc" })
  .paginate(1, 25);

// As a Record<string, string> (great for axios `params`)
query.toParams();
// { select: '["id","first_name","email"]',
//   filter: '{"and":[{"field":"state","op":"=","value":"Open"},{"field":"type","op":"in","value":["Buyer","Tenant"]}]}',
//   sort: '{"created_at":"desc"}', page: 1, limit: 25, ... }

// As a URL-encoded query string
query.toQueryParams();
// select=%5B...%5D&filter=%7B...%7D&sort=%7B...%7D&page=1&limit=25

Framework usage

The builder is plain TypeScript with zero framework coupling — import it the same way anywhere.

React

import { useMemo } from "react";
import { QueryBuilder } from "laravel-query-builder-js";

function useLeads(status: string, page: number) {
  const params = useMemo(
    () =>
      new QueryBuilder()
        .where("state", "=", status)
        .sort({ createdAt: "desc" })
        .paginate(page, 20)
        .toParams(),
    [status, page]
  );

  return useQuery(["leads", params], () =>
    fetch("/api/leads?" + new URLSearchParams(params)).then((r) => r.json())
  );
}

Vue 3

import { computed } from "vue";
import { QueryBuilder } from "laravel-query-builder-js";

const status = ref("Open");

const queryString = computed(() =>
  new QueryBuilder().where("state", "=", status.value).paginate(1, 20).toQueryParams()
);

watchEffect(() => fetch(`/api/leads?${queryString.value}`));

Angular

import { HttpClient } from "@angular/common/http";
import { QueryBuilder } from "laravel-query-builder-js";

@Injectable({ providedIn: "root" })
export class LeadService {
  constructor(private http: HttpClient) {}

  getLeads(status: string, page = 1) {
    const query = new QueryBuilder().where("state", "=", status).paginate(page, 20);
    return this.http.get("/api/leads", { params: query.toParams() });
  }
}

Plain fetch / Node

import { QueryBuilder } from "laravel-query-builder-js";

const query = new QueryBuilder().whereIn("id", [1, 2, 3]);
const res = await fetch(`https://api.example.com/users?${query.toQueryParams()}`);

Laravel backend (qubuilder)

This package is fully compatible with qubuilder, a PHP 8.3+ / Laravel 11+ package that consumes the exact payload produced by toParams() / toQueryParams() and turns it into a fully-chained Eloquent query — no manual if chains. Together they give you an end-to-end, Laravel-style query pipeline: compose on the client with laravel-query-builder-js, execute on the server with qubuilder.

The select, filter, sort, include and pagination keys map 1:1, so you don't have to translate anything between the two layers.

// Frontend — laravel-query-builder-js
const params = new QueryBuilder()
  .select(["id", "firstName", "email"])
  .where("state", "=", "Open")
  .sort({ createdAt: "desc" })
  .paginate(1, 25)
  .toParams();

await fetch("/api/leads?" + new URLSearchParams(params));
// Backend — qubuilder (Laravel controller)
use Kalimulhaq\Qubuilder\Qubuilder;

public function index(GetCollectionRequest $request)
{
    return Qubuilder::make($request->validated(), Lead::class)
        ->query()
        ->paginate();
}

See the qubuilder documentation for the full list of supported operators, aggregates and configuration.

API reference

Create a builder with the constructor or a static factory:

new QueryBuilder();
QueryBuilder.create("lead"); // optional resource name
QueryBuilder.new("lead"); // alias of create

Every mutating method returns this, so calls chain.

Filtering

q.where("state", "=", "Open"); // and: field = value
q.where("age", ">", 18);
q.orWhere("state", "=", "Pending"); // or: field = value
q.whereIn("type", ["Buyer", "Tenant"]); // and: field IN [...]
q.whereNull("deleted_at"); // and: field IS NULL
q.whereNotNull("email"); // and: field IS NOT NULL

Adding a where with the same field and operator replaces the previous one (so you can safely re-apply a filter as inputs change):

q.where("state", "=", "Open").where("state", "=", "Closed");
// -> only { field: "state", op: "=", value: "Closed" }

Remove clauses again:

q.clearWhere("currency"); // remove and-clause(s) on a field
q.clearWhere("state", "="); // remove only the "=" clause on a field
q.clearOrWhere("state"); // remove or-clause(s) on a field
q.resetFilter(); // drop all filters

Grouped & nested conditions

Pass a callback to build a parenthesised group. The nested builder exposes the same where / orWhere methods:

// WHERE (division_id = 5 OR division_id IS NULL)
q.where((sub) => sub.orWhere("division_id", "=", 5).orWhere("division_id", "null"));

Produces:

{ "and": [ { "or": [
  { "field": "division_id", "op": "=", "value": 5 },
  { "field": "division_id", "op": "null" }
] } ] }

Selecting fields

Field names are converted to snake_case automatically:

q.select("firstName,lastName"); // -> ["first_name", "last_name"]
q.select(["id", "createdAt"]); // -> ["id", "created_at"]
q.addSelect(["extra_field"]); // append (raw, not snake_cased)
q.resetSelect(); // clear

Sorting

Sort keys are snake_cased when serialised:

q.sort({ createdAt: "desc" }); // set (replaces)
q.addSort({ name: "asc" }); // merge into existing
q.resetSort(); // clear

Relations (include / withRelation)

Eager-load related resources with their own nested select / filter / sort / include using withRelation. Use the static QueryBuilder.withRelation(name) or the instance q.withRelation(name).

const q = new QueryBuilder();

q.addInclude(
  q.withRelation("owner").where("status", "=", "Active").select(["id", "name"])
);

// Deeply nested relations
q.addInclude([
  q
    .withRelation("portal_profiles")
    .addInclude(q.withRelation("portal"))
    .addInclude(q.withRelation("plugin_config")),
]);

// Aggregates on a relation
q.addInclude(q.withRelation("deals").aggregate("count"));

Methods:

  • include(rel | rel[]) — set the includes (replaces).
  • addInclude(rel | rel[]) — append includes.
  • resetInclude() — clear.
  • withRelation(name) — start a nested relation builder (chainable, same API).
  • .aggregate(value) — set an aggregate on a relation.

Pagination

q.paginate(2, 25); // page = 2, limit = 25 (limit defaults to 15)
q.fromPage(3); // set only the page
q.page = 4; // accessor
q.limit = 50; // accessor
q.nextPage(); // increment page
q.previousPage(); // decrement page
q.resetPage(); // back to page 1

Serialisation

q.toParams(); // Record<string, string> — for axios `params` / URLSearchParams
q.toQueryParams(); // URL-encoded query string "a=...&b=..."
q.getQuery(); // the raw internal query object

Utilities

q.clone(); // independent deep copy
q.addQueryBuilder(other); // merge another builder's select/filter/sort/include
q.resetQuery(); // reset everything (and re-apply default pagination)

Operators

The op argument accepts any of:

%   has   >   <   =   !=   <>   >=   <=   null   not_null
LIKE  like  BETWEEN  IN  in  has|=  has|<  has|>
any  any|like  ANY  doesntHave  _like_

Serialised output shape

toParams() returns a Record<string, string> where each part is JSON-encoded:

| Key | When present | Example value | | ----------- | ------------------------------- | -------------------------------------------------- | | select | any select fields set | ["id","first_name"] | | filter | any filter set | {"and":[{"field":"state","op":"=","value":"Open"}]} | | sort | any sort set | {"created_at":"desc"} (keys snake_cased) | | include | any relation added | [{"name":"owner","filter":{...}}] | | page | page is truthy | 2 | | limit | limit is truthy | 25 |

toQueryParams() returns the same information as a single URL-encoded string (select=...&filter=...&sort=...&include=...&page=...&limit=...).

TypeScript

Fully typed. Public types are exported for building your own helpers:

import type {
  Op,
  Query,
  QueryFilter,
  QueryFilterGroup,
  QueryInclude,
  QuerySort,
} from "laravel-query-builder-js";

Contributing & development

git clone https://github.com/RanaUsman3131/laravel-query-builder-js.git
cd laravel-query-builder-js
npm install

npm test           # run the unit tests once
npm run test:watch # watch mode
npm run typecheck  # tsc --noEmit
npm run build      # bundle to dist/ (ESM + CJS + d.ts)

Please add tests for any new behaviour.

Releasing

Releases are automated by GitHub Actions. Pushing a version tag builds, tests, publishes to npm (with provenance), and creates a GitHub Release.

# bump the version (updates package.json and creates the commit + tag)
npm version patch        # or minor / major
git push --follow-tags

Or tag manually — the tag must match package.json's version:

git tag v1.2.3
git push origin v1.2.3

One-time setup: add an npm Automation access token as the repository secret NPM_TOKEN (Settings → Secrets and variables → Actions). Provenance requires the repository to be public.

License

MIT © Rana Usman / Engage Platform