NestJS Integration
Guide for using @mcabreradev/filter with NestJS.
Overview
@mcabreradev/filter integrates seamlessly with NestJS applications, providing powerful filtering capabilities for controllers, services, GraphQL resolvers, and more.
Installation
bash
npm install @mcabreradev/filter
npm install --save-dev @types/node
pnpm add @mcabreradev/filter
pnpm add -D @types/node
yarn add @mcabreradev/filter
yarn add -D @types/nodeController Integration
Basic Controller
typescript
import { Controller, Get, Query } from '@nestjs/common';
import { filter } from '@mcabreradev/filter';
import type { Expression } from '@mcabreradev/filter';
interface User {
id: number;
name: string;
email: string;
status: 'active' | 'inactive';
role: string;
}
@Controller('users')
export class UsersController {
private users: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com', status: 'active', role: 'admin' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'active', role: 'user' }
];
@Get()
findAll(@Query('status') status?: string, @Query('role') role?: string) {
const conditions: any[] = [];
if (status) {
conditions.push({ status: { $eq: status } });
}
if (role) {
conditions.push({ role: { $eq: role } });
}
const expression: Expression<User> =
conditions.length > 0 ? { $and: conditions } : {};
const filtered = filter(this.users, expression);
return {
success: true,
data: filtered,
count: filtered.length
};
}
}Controller with DTO
typescript
import { Controller, Get, Post, Body, Query } from '@nestjs/common';
import { IsOptional, IsString, IsEnum } from 'class-validator';
export class FilterQueryDto {
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsString()
role?: string;
@IsOptional()
@IsString()
search?: string;
}
export class FilterExpressionDto {
@IsOptional()
expression?: Expression<User>;
}
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll(@Query() query: FilterQueryDto) {
return this.usersService.findFiltered(query);
}
@Post('filter')
async filterUsers(@Body() dto: FilterExpressionDto) {
return this.usersService.filterByExpression(dto.expression);
}
}Service Layer Filtering
Basic Service
typescript
import { Injectable } from '@nestjs/common';
import { filter } from '@mcabreradev/filter';
import type { Expression } from '@mcabreradev/filter';
@Injectable()
export class UsersService {
private users: User[] = [];
findFiltered(query: FilterQueryDto) {
const conditions: any[] = [];
if (query.status) {
conditions.push({ status: { $eq: query.status } });
}
if (query.role) {
conditions.push({ role: { $eq: query.role } });
}
if (query.search) {
conditions.push({
$or: [
{ name: { $regex: new RegExp(query.search, 'i') } },
{ email: { $regex: new RegExp(query.search, 'i') } }
]
});
}
const expression: Expression<User> =
conditions.length > 0 ? { $and: conditions } : {};
const filtered = filter(this.users, expression, {
memoize: true
});
return {
success: true,
data: filtered,
count: filtered.length
};
}
filterByExpression(expression: Expression<User>) {
const filtered = filter(this.users, expression);
return {
success: true,
data: filtered,
count: filtered.length
};
}
}Service with Repository Pattern
typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findFiltered(query: FilterQueryDto) {
const allUsers = await this.usersRepository.find();
const conditions: any[] = [];
if (query.status) {
conditions.push({ status: { $eq: query.status } });
}
if (query.role) {
conditions.push({ role: { $eq: query.role } });
}
const expression: Expression<User> =
conditions.length > 0 ? { $and: conditions } : {};
const filtered = filter(allUsers, expression);
return {
success: true,
data: filtered,
count: filtered.length
};
}
}DTO Validation with Filtering
Filter DTO
typescript
import { IsOptional, IsString, IsNumber, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
export class PaginatedFilterDto {
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsString()
role?: string;
@IsOptional()
@IsString()
search?: string;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
@Max(100)
limit?: number = 10;
}Using DTO in Controller
typescript
import { Controller, Get, Query, ValidationPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll(
@Query(new ValidationPipe({ transform: true }))
query: PaginatedFilterDto
) {
return this.usersService.findPaginated(query);
}
}Pipes and Guards
Filter Validation Pipe
typescript
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class FilterExpressionPipe implements PipeTransform {
transform(value: any) {
if (!value || typeof value !== 'object') {
throw new BadRequestException('Invalid filter expression');
}
return value;
}
}
@Controller('users')
export class UsersController {
@Post('filter')
async filterUsers(
@Body('expression', FilterExpressionPipe) expression: Expression<User>
) {
return this.usersService.filterByExpression(expression);
}
}Role-Based Filter Guard
typescript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class FilterGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
return false;
}
if (user.role !== 'admin') {
request.query.userId = user.id;
}
return true;
}
}
@Controller('users')
@UseGuards(FilterGuard)
export class UsersController {
@Get()
async findAll(@Query() query: FilterQueryDto) {
return this.usersService.findFiltered(query);
}
}Database Integration
TypeORM Integration
typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { filter } from '@mcabreradev/filter';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findWithFilter(query: FilterQueryDto) {
const users = await this.usersRepository.find();
const conditions: any[] = [];
if (query.status) {
conditions.push({ status: { $eq: query.status } });
}
if (query.role) {
conditions.push({ role: { $eq: query.role } });
}
const expression = conditions.length > 0 ? { $and: conditions } : {};
return filter(users, expression);
}
}Prisma Integration
typescript
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { filter } from '@mcabreradev/filter';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async findWithFilter(query: FilterQueryDto) {
const users = await this.prisma.user.findMany();
const conditions: any[] = [];
if (query.status) {
conditions.push({ status: { $eq: query.status } });
}
if (query.role) {
conditions.push({ role: { $eq: query.role } });
}
const expression = conditions.length > 0 ? { $and: conditions } : {};
return filter(users, expression);
}
}GraphQL Resolver Filtering
Basic Resolver
typescript
import { Resolver, Query, Args } from '@nestjs/graphql';
import { filter } from '@mcabreradev/filter';
import { User } from './models/user.model';
@Resolver(() => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query(() => [User])
async users(
@Args('status', { nullable: true }) status?: string,
@Args('role', { nullable: true }) role?: string,
) {
const allUsers = await this.usersService.findAll();
const conditions: any[] = [];
if (status) {
conditions.push({ status: { $eq: status } });
}
if (role) {
conditions.push({ role: { $eq: role } });
}
const expression = conditions.length > 0 ? { $and: conditions } : {};
return filter(allUsers, expression);
}
}Resolver with Input Type
typescript
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class FilterInput {
@Field({ nullable: true })
status?: string;
@Field({ nullable: true })
role?: string;
@Field({ nullable: true })
search?: string;
}
@Resolver(() => User)
export class UsersResolver {
@Query(() => [User])
async filteredUsers(@Args('filter') filterInput: FilterInput) {
const allUsers = await this.usersService.findAll();
const conditions: any[] = [];
if (filterInput.status) {
conditions.push({ status: { $eq: filterInput.status } });
}
if (filterInput.role) {
conditions.push({ role: { $eq: filterInput.role } });
}
if (filterInput.search) {
conditions.push({
$or: [
{ name: { $regex: new RegExp(filterInput.search, 'i') } },
{ email: { $regex: new RegExp(filterInput.search, 'i') } }
]
});
}
const expression = conditions.length > 0 ? { $and: conditions } : {};
return filter(allUsers, expression);
}
}Advanced Patterns
Custom Filter Decorator
typescript
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import type { Expression } from '@mcabreradev/filter';
export const FilterExpression = createParamDecorator(
(data: unknown, ctx: ExecutionContext): Expression<any> => {
const request = ctx.switchToHttp().getRequest();
const query = request.query;
const conditions: any[] = [];
Object.keys(query).forEach(key => {
if (query[key]) {
conditions.push({ [key]: { $eq: query[key] } });
}
});
return conditions.length > 0 ? { $and: conditions } : {};
},
);
@Controller('users')
export class UsersController {
@Get()
async findAll(@FilterExpression() expression: Expression<User>) {
return this.usersService.filterByExpression(expression);
}
}Filter Interceptor
typescript
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { filter } from '@mcabreradev/filter';
@Injectable()
export class FilterInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const filterQuery = request.query.filter;
return next.handle().pipe(
map(data => {
if (!filterQuery || !Array.isArray(data)) {
return data;
}
try {
const expression = JSON.parse(filterQuery);
return filter(data, expression);
} catch {
return data;
}
}),
);
}
}
@Controller('users')
@UseInterceptors(FilterInterceptor)
export class UsersController {
@Get()
async findAll() {
return this.usersService.findAll();
}
}Dynamic Filter Building from DTOs
typescript
import { Injectable } from '@nestjs/common';
import type { Expression } from '@mcabreradev/filter';
@Injectable()
export class FilterBuilderService {
buildExpression<T>(dto: Record<string, any>): Expression<T> {
const conditions: any[] = [];
Object.entries(dto).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (typeof value === 'string' && value.includes('*')) {
const regex = new RegExp(value.replace(/\*/g, '.*'), 'i');
conditions.push({ [key]: { $regex: regex } });
} else if (Array.isArray(value)) {
conditions.push({ [key]: { $in: value } });
} else {
conditions.push({ [key]: { $eq: value } });
}
});
return conditions.length > 0 ? { $and: conditions } : {};
}
}
@Injectable()
export class UsersService {
constructor(private filterBuilder: FilterBuilderService) {}
async findFiltered(query: FilterQueryDto) {
const users = await this.findAll();
const expression = this.filterBuilder.buildExpression<User>(query);
return filter(users, expression);
}
}Role-Based Filtering
typescript
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
async findFilteredByRole(query: FilterQueryDto, userRole: string, userId: number) {
const users = await this.findAll();
const conditions: any[] = [];
if (query.status) {
conditions.push({ status: { $eq: query.status } });
}
if (userRole !== 'admin') {
conditions.push({ id: { $eq: userId } });
}
const expression = conditions.length > 0 ? { $and: conditions } : {};
return filter(users, expression);
}
}
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
@UseGuards(JwtAuthGuard)
async findAll(
@Query() query: FilterQueryDto,
@Request() req,
) {
return this.usersService.findFilteredByRole(
query,
req.user.role,
req.user.id
);
}
}Caching with Filtering
typescript
import { Injectable, CacheInterceptor, UseInterceptors } from '@nestjs/common';
import { CacheKey, CacheTTL } from '@nestjs/cache-manager';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
@UseInterceptors(CacheInterceptor)
@CacheKey('filtered-users')
@CacheTTL(300)
async findAll(@Query() query: FilterQueryDto) {
return this.usersService.findFiltered(query);
}
}Testing with Jest
Service Testing
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should filter users by status', () => {
const result = service.findFiltered({ status: 'active' });
expect(result.data.every(u => u.status === 'active')).toBe(true);
});
it('should filter users by multiple conditions', () => {
const result = service.findFiltered({
status: 'active',
role: 'admin'
});
expect(result.data).toHaveLength(1);
expect(result.data[0].role).toBe('admin');
});
});Controller Testing
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [UsersService],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
it('should return filtered users', async () => {
const result = await controller.findAll({ status: 'active' });
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
});
});E2E Testing
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/users (GET) with filters', () => {
return request(app.getHttpServer())
.get('/users?status=active&role=admin')
.expect(200)
.expect(res => {
expect(res.body.success).toBe(true);
expect(res.body.data).toBeDefined();
});
});
afterAll(async () => {
await app.close();
});
});Performance Optimization
1. Enable Memoization
typescript
@Injectable()
export class UsersService {
findFiltered(query: FilterQueryDto) {
const filtered = filter(users, expression, {
memoize: true
});
return filtered;
}
}2. Use Caching
typescript
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class UsersService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async findFiltered(query: FilterQueryDto) {
const cacheKey = JSON.stringify(query);
const cached = await this.cacheManager.get(cacheKey);
if (cached) {
return cached;
}
const filtered = filter(users, expression);
await this.cacheManager.set(cacheKey, filtered, 300);
return filtered;
}
}3. Implement Pagination
typescript
@Injectable()
export class UsersService {
findPaginated(query: PaginatedFilterDto) {
const filtered = filter(users, expression);
const startIndex = (query.page - 1) * query.limit;
const endIndex = startIndex + query.limit;
const paginated = filtered.slice(startIndex, endIndex);
return {
data: paginated,
pagination: {
page: query.page,
limit: query.limit,
total: filtered.length,
totalPages: Math.ceil(filtered.length / query.limit)
}
};
}
}Complete Example
typescript
import { Module, Controller, Get, Post, Body, Query, Injectable } from '@nestjs/common';
import { filter } from '@mcabreradev/filter';
import type { Expression } from '@mcabreradev/filter';
interface User {
id: number;
name: string;
email: string;
status: 'active' | 'inactive';
role: string;
}
@Injectable()
export class UsersService {
private users: User[] = [];
findFiltered(query: any) {
const conditions: any[] = [];
if (query.status) conditions.push({ status: { $eq: query.status } });
if (query.role) conditions.push({ role: { $eq: query.role } });
if (query.search) {
conditions.push({
$or: [
{ name: { $regex: new RegExp(query.search, 'i') } },
{ email: { $regex: new RegExp(query.search, 'i') } }
]
});
}
const expression: Expression<User> =
conditions.length > 0 ? { $and: conditions } : {};
return filter(this.users, expression, { memoize: true });
}
}
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
findAll(@Query() query: any) {
const filtered = this.usersService.findFiltered(query);
return { success: true, data: filtered };
}
@Post('filter')
filterUsers(@Body('expression') expression: Expression<User>) {
const filtered = filter(this.usersService['users'], expression);
return { success: true, data: filtered };
}
}
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}