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.