In the fast-paced world of Node.js and MongoDB development, writing complex queries can become a tedious and error-prone task. Whether you're building REST APIs, GraphQL endpoints, or intricate multi-tenant systems, you need a solution that simplifies and streamlines your database interactions. Enter @hshanjra/mongoose-query-builder – a powerful, TypeScript-first query builder for Mongoose that promises a clean, intuitive interface to build complex MongoDB queries effortlessly [].

In this comprehensive guide, we’ll explore all the features this package provides, discuss why it might be a game-changer for your project, and provide practical examples to get you started.

Table of Contents

  • Overview and Key Benefits
  • Core Features
  • Dynamic Model Loading
  • TypeScript Support
  • Rich Filtering and MongoDB-Style Operators
  • Smart Pagination with Metadata
  • Flexible Sorting Options
  • Deep Population and Field Selection
  • Full-Text Search Capabilities
  • Default and Restricted Filters
  • Framework Integrations
  • Installation and Setup
  • Usage Examples
  • Basic Query Construction
  • Advanced Query Configurations
  • Integration with Express and NestJS
  • Conclusion

Overview and Key Benefits

Modern web applications demand efficient and maintainable data access layers. @hshanjra/mongoose-query-builder has been designed to address these needs by providing:

A clean, expressive API: Simplify your query building process with an intuitive interface.

Type safety: Full TypeScript support ensures that your queries and responses are type-safe, reducing runtime errors.

Enhanced productivity: Reduce boilerplate code for common operations such as filtering, sorting, and pagination.

Decoupled architecture: With dynamic model loading, you can refer to models by name rather than importing them directly, enhancing modularity.

Seamless integration: Whether you’re using Express, NestJS, or vanilla Node.js, this package fits right into your workflow.

Core Features

Dynamic Model Loading

One of the standout features is dynamic model loading. Instead of importing your Mongoose models explicitly, you simply reference them by name. This approach improves decoupling and keeps your codebase flexible—especially beneficial in large-scale or multi-tenant applications.

TypeScript Support

Developers working in TypeScript will appreciate the built-in type declarations. This package provides generic types for both documents and query responses, enabling compile-time checking and better code completion in your IDE.

Rich Filtering and MongoDB-Style Operators

The package offers a comprehensive filtering API that mimics MongoDB’s query operators with an easier-to-read syntax. For example:

const { data } = await query.graph({
  entity: "Product",
  filters: {
    // Greater than or equal to 100
    price_gte: 100,
    // Less than or equal to 500
    price_lte: 500,
    // In array operator for categories
    category_in: ["electronics", "gadgets"],
    // Regex match for product names
    name_regex: "^iPhone",
  },
});

This rich filtering capability means you can combine multiple query operators effortlessly and build even the most complex filters in a clean, understandable manner.

Smart Pagination with Metadata

Pagination is built-in, and it does more than just split your results into pages. It returns detailed metadata such as the current page, total number of documents, page size, and even indicators for previous and next pages. This metadata is extremely useful for creating user-friendly pagination interfaces in your applications.

Flexible Sorting Options

Sorting can be performed in multiple formats, giving you the flexibility to sort by one or multiple fields. You can define sorting as a simple string, an array of strings, or even as objects with explicit ordering:

// Simple string format
sort: "price:desc",

// Multiple sort criteria using array of strings
sort: ["price:desc", "name:asc"],

// Object format with type safety
sort: [
  { field: "price", order: "desc" },
  { field: "name", order: "asc" },
],

Deep Population and Field Selection

For applications that need to resolve nested relationships, deep population is a key feature. You can populate nested fields and even control which fields are selected from the related documents:

const { data } = await query.graph({
  entity: "Order",
  expand: [
    { path: "customer", select: ["name", "email"] },
    { path: "items", select: ["name", "price", "quantity"] },
  ],
});

Field selection goes hand in hand with population, allowing you to return only the necessary data and reduce payload sizes.

Full-Text Search Capabilities

Full-text search is integrated directly into the query builder. With support for language settings, case sensitivity, and diacritic sensitivity, you can implement powerful search features in your application with minimal effort:

const { data } = await query.graph({
  entity: "Product",
  fullTextSearch: {
    searchText: "wireless headphones",
    language: "english",
    sortByScore: true,
    caseSensitive: false,
  },
});

Default and Restricted Filters

You can define default filters that are always applied to every query (such as multi-tenant restrictions) as well as restrict which fields can be queried. This ensures that sensitive or non-indexed fields are protected from unauthorized access or inefficient queries.

Framework Integrations

Whether you are using Express, NestJS, or even a vanilla Node.js/TypeScript application, this package integrates seamlessly. It also comes with built-in middleware for Express, which automatically parses incoming query parameters and transforms them into a standardized query configuration.

Installation and Setup

Before you get started, make sure you have Mongoose installed as a peer dependency:

npm install mongoose @hshanjra/mongoose-query-builder

Once installed, you can start using it in your project by importing the QueryBuilder:

import { QueryBuilder } from "@hshanjra/mongoose-query-builder";

Usage Examples

Basic Query Construction

Below is a simple example that demonstrates how to construct a query for fetching users with certain filters:

import { QueryBuilder } from "@hshanjra/mongoose-query-builder";
import { Document } from "mongoose";

// Define your document interface
interface UserDocument extends Document {
  name: string;
  email: string;
  age: number;
  status: string;
}

(async () => {
  // Initialize the query builder
  const query = new QueryBuilder();

  // Execute a type-safe query
  const { data, metadata } = await query.graph({
    entity: "User", // Use model name string
    fields: ["name", "email", "age"],
    filters: {
      age_gte: 18,
      status: "active",
    },
    pagination: {
      page: 1,
      limit: 10,
    },
  });

  console.log("Users:", data);
  console.log("Metadata:", metadata);
})();

Advanced Query Configurations

This example illustrates the flexibility of the query builder by combining various features—filtering, sorting, and full-text search:

const { data, metadata } = await query.graph({
  entity: "Product",
  fields: ["name", "price", "category"],
  filters: {
    price_gte: 100,
    price_lte: 1000,
    category: "electronics",
  },
  sort: [
    { field: "price", order: "desc" },
    { field: "name", order: "asc" },
  ],
  fullTextSearch: {
    searchText: "smartphone",
    language: "english",
    sortByScore: true,
    caseSensitive: false,
  },
  pagination: {
    page: 2,
    limit: 20,
  },
});

console.log("Products:", data);
console.log("Pagination Info:", metadata);

Integration with Express and NestJS

Express Middleware Example

Integrating with Express is straightforward using the provided middleware. This middleware automatically converts URL query parameters into a format suitable for the query builder:

import express from "express";
import { QueryBuilder } from "@hshanjra/mongoose-query-builder";
import { QueryBuilderMiddleware } from "@hshanjra/mongoose-query-builder/middlewares";

const app = express();

// Configure middleware with options like maxLimit and restricted fields
app.use(
  QueryBuilderMiddleware({
    maxLimit: 50,
    defaultLimit: 20,
    restrictedFields: ["password", "secretKey"],
  })
);

app.get("/api/products", async (req, res) => {
  try {
    const query = new QueryBuilder();
    const { data, metadata } = await query.graph({
      entity: "Product",
      ...req.queryOptions,
      defaultFilters: {
        isActive: true,
        isDeleted: false,
      },
    });
    res.json({ data, metadata });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => console.log("Server running on port 3000"));

NestJS Integration Example

For NestJS applications, you can create a dedicated service that encapsulates the query builder logic:

import { Injectable } from "@nestjs/common";
import { InjectConnection } from "@nestjs/mongoose";
import { Connection } from "mongoose";
import { QueryBuilder } from "@hshanjra/mongoose-query-builder";
import { GraphQueryConfig, GraphQueryResponse } from "@hshanjra/mongoose-query-builder/types";

@Injectable()
export class QueryBuilderService {
  private queryBuilder: QueryBuilder;

  constructor(@InjectConnection() private connection: Connection) {
    this.queryBuilder = new QueryBuilder(connection);
  }

  async graph(config: GraphQueryConfig): Promise> {
    return this.queryBuilder.graph(config);
  }
}

Then, inject this service into your controllers to build dynamic queries based on incoming requests.

Conclusion

@hshanjra/mongoose-query-builder is a modern, feature-rich solution that can greatly simplify the way you interact with MongoDB using Mongoose. Its TypeScript-first approach ensures type safety and enhanced developer productivity, while its flexible API supports advanced features like rich filtering, smart pagination, deep population, and full-text search. Whether you're building a small project or a large-scale enterprise application, this query builder can help you write cleaner, more maintainable code with less hassle.

By integrating seamlessly with popular frameworks like Express and NestJS, it provides the versatility required for today's complex applications. If you’re looking to reduce boilerplate and enhance the clarity of your database queries, this package is well worth a closer look.

Happy coding, and enjoy building efficient, scalable applications with @hshanjra/mongoose-query-builder!

For more details and the latest updates, visit the GitHub repository or the npm package page.