@rabstack/rab-query
v0.6.0
Published
Type-safe, bidirectional query string parser for building REST APIs with rich filtering capabilities
Readme
RabQuery
Type-safe, bidirectional query string parser for building REST APIs with rich filtering capabilities.
Parse query strings into typed objects (Server) and encode objects into query strings (Client) with operator syntax inspired by Prisma, supporting nested objects and logical conditions.
Features
- Bidirectional: Client → Query String ↔ Server Object
- Type-Safe: Full TypeScript support with generics
- Rich Operators: 19+ operators with syntax inspired by Prisma
- Nested Objects: Deep filtering with dot notation (
user.profile.age) - Logical Operators:
OR,AND,NOTconditions - Sorting: Nested
orderBysupport - Zero Dependencies: Lightweight and fast
- Framework Agnostic: Works with any Node.js framework or database
Installation
npm install @rabstack/rab-query
# or
yarn add @rabstack/rab-query
# or
pnpm add @rabstack/rab-queryQuick Start
Client Side (Build Query Strings)
import { encodeRabQuery } from '@rabstack/rab-query';
// Build a query string from an object
const queryString = encodeRabQuery({
name: { contains: 'John' },
age: { gte: 18 },
status: { in: ['active', 'pending'] },
orderBy: [{ createdAt: 'desc' }]
});
// Result: "name__contains=John&age__gte=18&status__in=active,pending&orderBy=createdAt__desc"Server Side (Parse Query Strings)
import { buildQueryParameters } from '@rabstack/rab-query';
// Express.js example
app.get('/users', (req, res) => {
const params = buildQueryParameters(req.query);
// Use with Prisma
const users = await prisma.user.findMany({
where: params,
orderBy: params.orderBy
});
res.json(users);
});Note: rab-query focuses on filtering and sorting. Pagination (page, pageSize) is intentionally not included to keep the library generic - implement pagination according to your specific needs.
API Reference
Core Functions
buildQueryParameters<T>(filters: Record<string, any>): RabQueryParams<T>
Parses URL query parameters into a typed object suitable for database queries.
Server-side usage: Parse incoming request query strings.
// URL: ?name__contains=john&age__gte=18
// Express automatically parses to: req.query = { name__contains: 'john', age__gte: '18' }
const params = buildQueryParameters(req.query);
// Result:
{
name: { contains: 'john', mode: 'insensitive' },
age: { gte: 18 }
}encodeRabQuery<T>(query: RabQueryParams<T>): string
Encodes a typed object into a URL query string.
Client-side usage: Build query strings for API requests.
const queryString = encodeRabQuery({
name: { contains: 'John' },
age: { gte: 18 }
});
// Output: "name__contains=John&age__gte=18"Supported Operators
Operator syntax inspired by Prisma:
Comparison Operators
equals- Exact matchnot- Not equalin- Match any value in arraynotIn- Match none of the values in arraylt- Less thanlte- Less than or equalgt- Greater thangte- Greater than or equal
String Operators
contains- Contains substring (case-insensitive)startsWith- Starts with substringendsWith- Ends with substringsearch- Full-text search
Array/List Operators
has- Array contains valuehasSome- Array contains some valueshasEvery- Array contains all valuesisEmpty- Array is empty
Null Checks
isSet- Field is set (not null)
Usage Examples
Basic Filtering
// Simple equality
const params = buildQueryParameters({ name: 'John' });
// → { name: 'John' }
// With operator
const params = buildQueryParameters({ 'age__gte': '18' });
// → { age: { gte: 18 } }Nested Objects
Use dot notation for deep filtering:
// Client side
const queryString = encodeRabQuery({
user: {
profile: {
age: { gte: 18 }
}
}
});
// → "user.profile.age__gte=18"
// Server side
const params = buildQueryParameters({ 'user.profile.age__gte': '18' });
// → { user: { profile: { age: { gte: 18 } } } }Array Operators
// IN operator (comma-separated values)
const params = buildQueryParameters({ 'status__in': 'active,pending' });
// → { status: { in: ['active', 'pending'] } }
// Array contains
const params = buildQueryParameters({ 'tags__hasSome': 'javascript,typescript' });
// → { tags: { hasSome: ['javascript', 'typescript'] } }Logical Operators (OR, AND, NOT)
// OR condition - append __or to any operator
const params = buildQueryParameters({
'age__gt__or': '18',
'age__lt__or': '65'
});
// → { OR: [{ age: { gt: 18 } }, { age: { lt: 65 } }] }
// Client side - build OR queries
const queryString = encodeRabQuery({
OR: [
{ age: { gt: 18 } },
{ status: 'premium' }
]
});
// → "age__gt__or=18&status__or=premium"Sorting (OrderBy)
// Single field
const params = buildQueryParameters({ 'orderBy': 'createdAt__desc' });
// → { orderBy: [{ createdAt: 'desc' }] }
// Multiple fields (comma-separated)
const params = buildQueryParameters({ 'orderBy': 'name__asc,createdAt__desc' });
// → { orderBy: [{ name: 'asc' }, { createdAt: 'desc' }] }
// Nested fields
const params = buildQueryParameters({ 'orderBy': 'user.profile.age__desc' });
// → { orderBy: [{ user: { profile: { age: 'desc' } } }] }
// Client side - array format (multiple fields)
const queryString = encodeRabQuery({
orderBy: [
{ name: 'asc' },
{ createdAt: 'desc' }
]
});
// → "orderBy=name__asc,createdAt__desc"
// Client side - single object format (one field)
const queryString = encodeRabQuery({
orderBy: { name: 'asc' }
});
// → "orderBy=name__asc"
// Client side - single object with multiple fields (NEW!)
const queryString = encodeRabQuery({
orderBy: { name: 'asc', createdAt: 'desc' }
});
// → "orderBy=name__asc,createdAt__desc"
// All three formats work!
// Note: For critical sort priority, array format is recommended for explicit orderingComplete Example
// Client builds complex query
const queryString = encodeRabQuery({
name: { contains: 'John' },
age: { gte: 18, lte: 65 },
status: { in: ['active', 'pending'] },
user: {
profile: {
verified: true
}
},
OR: [
{ role: 'admin' },
{ credits: { gt: 100 } }
],
orderBy: [{ createdAt: 'desc' }]
});
// Server parses and uses with database
app.get('/users', async (req, res) => {
const params = buildQueryParameters(req.query);
// Implement your own pagination
const page = Number(req.query.page) || 1;
const pageSize = Math.min(Number(req.query.pageSize) || 10, 100);
const users = await prisma.user.findMany({
where: params,
take: pageSize,
skip: (page - 1) * pageSize,
orderBy: params.orderBy
});
res.json(users);
});TypeScript Support
RabQuery is fully typed with generics for your data models:
interface User {
id: string;
name: string;
age: number;
email: string;
profile: {
bio: string;
verified: boolean;
};
}
// Type-safe query building
const params: RabQueryParams<User> = buildQueryParameters(req.query);
// Type-safe encoding
const queryString = encodeRabQuery<User>({
name: { contains: 'John' },
age: { gte: 18 },
profile: {
verified: true
}
});
// Autocomplete and type checking!Core Types
import type {
RabQueryParams, // Main query type
BaseRabQuery, // Base query type with orderBy
RabQueryFilters, // Filter types
OrderByInput, // OrderBy type
FieldOperators, // Available operators per field
SupportedOperator, // All operator names
SortOrder, // 'asc' | 'desc'
} from '@rabstack/rab-query';Query String Format
RabQuery uses a specific format for query strings:
Basic Format
field__operator=valueNested Objects (dot notation)
user.profile.age__gte=18Conditions (suffix)
age__gt__or=18
status__equals__and=activeArrays (comma-separated)
status__in=active,pending,approved
tags__hasSome=javascript,typescriptMultiple Values (same key)
?age__gt__or=18&age__lt__or=65
// Results in: OR: [{ age: { gt: 18 } }, { age: { lt: 65 } }]Advanced Usage
Utility Functions
import {
parseQueryKey,
isSupportedOperator,
isArrayOperator,
isCaseInsensitiveOperator,
supportedOperators
} from '@rabstack/rab-query';
// Parse individual query keys
const parsed = parseQueryKey('user.age__gte__or');
// → { path: ['user', 'age'], modifier: { operator: 'gte', condition: 'or' } }
// Check operator types
isSupportedOperator('contains'); // true
isArrayOperator('in'); // true
isCaseInsensitiveOperator('contains'); // trueCustom Validation
import { buildQueryParameters, RabQueryParams } from '@rabstack/rab-query';
function validateAndParse<T>(query: any): RabQueryParams<T> {
const params = buildQueryParameters<T>(query);
// Add custom validation as needed
// Example: Validate specific fields or business rules
if (params.age && typeof params.age === 'object' && 'gte' in params.age) {
if (params.age.gte < 0) {
throw new Error('Age cannot be negative');
}
}
return params;
}Framework Integration
Express.js
import express from 'express';
import { buildQueryParameters } from '@rabstack/rab-query';
const app = express();
app.get('/api/users', async (req, res) => {
const params = buildQueryParameters(req.query);
const users = await db.users.find(params);
res.json(users);
});NestJS
import { Controller, Get, Query } from '@nestjs/common';
import { buildQueryParameters } from '@rabstack/rab-query';
@Controller('users')
export class UsersController {
@Get()
async findAll(@Query() query: any) {
const params = buildQueryParameters(query);
return this.usersService.findAll(params);
}
}Fastify
import Fastify from 'fastify';
import { buildQueryParameters } from '@rabstack/rab-query';
const fastify = Fastify();
fastify.get('/users', async (request, reply) => {
const params = buildQueryParameters(request.query);
return db.users.find(params);
});React Query (Client)
import { useQuery } from '@tanstack/react-query';
import { encodeRabQuery } from '@rabstack/rab-query';
function useUsers(filters: RabQueryParams<User>) {
return useQuery({
queryKey: ['users', filters],
queryFn: async () => {
const queryString = encodeRabQuery(filters);
const response = await fetch(`/api/users?${queryString}`);
return response.json();
}
});
}
// Usage
const { data } = useUsers({
name: { contains: 'John' },
age: { gte: 18 }
});Best Practices
- Always validate input: RabQuery parses query strings but doesn't validate business rules
- Implement pagination separately: Add
pageandpageSizehandling according to your needs - Use TypeScript: Leverage generic types for better type safety
- Sanitize user input: While RabQuery is safe from injection, always validate data
- Document your API: Share the query format with your API consumers
Roadmap
Future Enhancements
- Elasticsearch Adapter: Transform RabQuery output to Elasticsearch Query DSL
- Convert Prisma-style operators to Elasticsearch format
- Map
contains→match,gte/lte→range,in→terms - Transform
OR/AND/NOTtoboolqueries (should/must/must_not) - Maintain type safety and full query compatibility
Migration from V1
RabQuery V2 is backward compatible with V1. The main differences:
- New nested object support via dot notation
- Extended operator set (19 vs 8 operators)
- Improved TypeScript types
- New
encodeRabQueryfunction for client-side
Existing V1 queries will continue to work without changes.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT
