Skip to main content
Skip to main content

RequestOptionBuilder Utility

The RequestOptionBuilder utility provides methods for parsing and validating query parameters from HTTP requests, converting them into structured options for database queries.

Class Definition​

class RequestOptionBuilder {
static getDefaultQuerySchemas(): QuerySchemas
static validateQueryParam<T>(value: string, schema: z.ZodSchema<T>): T
static buildFindOptions(req: Request, querySchemas: QuerySchemas): any
static buildSearchOptions(req: Request, querySchemas: QuerySchemas): SearchOptions
}

Types​

QuerySchemas​

interface QuerySchemas {
includeSchema: ZodSchema<any>;
orderBySchema: ZodSchema<any>;
whereSchema: ZodSchema<any>;
paginationSchema: ZodSchema<{
page?: number;
pageSize?: number;
}>;
}

Methods​

getDefaultQuerySchemas(): QuerySchemas​

Returns default Zod schemas for validating query parameters.

Returns: QuerySchemas - Default validation schemas

Default Schemas:

{
includeSchema: z.record(z.any()).optional(),
orderBySchema: z.record(z.enum(['asc', 'desc'])).optional(),
whereSchema: z.record(z.any()).optional(),
paginationSchema: z.object({
page: z.number().int().positive(),
pageSize: z.number().int().positive().max(100),
}).strict()
}

Example:

const defaultSchemas = RequestOptionBuilder.getDefaultQuerySchemas();

// Customize schemas
const customSchemas = {
...defaultSchemas,
whereSchema: z.object({
active: z.boolean().optional(),
role: z.string().optional(),
createdAt: z.object({
gte: z.date().optional(),
lte: z.date().optional()
}).optional()
}).optional()
};

validateQueryParam<T>(value: string, schema: ZodSchema<T>): T​

Validates and parses a JSON query parameter using a Zod schema.

Parameters:

  • value: string - JSON string from query parameter
  • schema: ZodSchema<T> - Zod schema for validation

Returns: T - Parsed and validated value

Throws: BadRequestException for invalid JSON or validation errors

Example:

// URL: /users?where={"active":true,"role":"user"}
const whereParam = req.query.where as string;
const whereClause = RequestOptionBuilder.validateQueryParam(
whereParam,
z.object({
active: z.boolean().optional(),
role: z.string().optional()
})
);
// Result: { active: true, role: "user" }

buildFindOptions(req: Request, querySchemas: QuerySchemas): any​

Builds query options for simple find operations (without pagination).

Parameters:

  • req: Request - Express request object
  • querySchemas: QuerySchemas - Validation schemas

Returns: Query options object with include, orderBy, and where properties

Supported Query Parameters:

  • include - JSON string for related data inclusion
  • orderBy - JSON string for sorting criteria
  • where - JSON string for filtering criteria

Example:

// URL: /users?include={"profile":true}&orderBy={"createdAt":"desc"}&where={"active":true}

const options = RequestOptionBuilder.buildFindOptions(req, querySchemas);
// Result:
// {
// include: { profile: true },
// orderBy: { createdAt: 'desc' },
// where: { active: true }
// }

const users = await userRepository.findMany(options);

buildSearchOptions(req: Request, querySchemas: QuerySchemas): SearchOptions​

Builds comprehensive search options including pagination parameters.

Parameters:

  • req: Request - Express request object
  • querySchemas: QuerySchemas - Validation schemas

Returns: SearchOptions - Complete search options with pagination

Supported Query Parameters:

  • page - Page number (integer)
  • pageSize - Items per page (integer, max 100)
  • include - JSON string for related data inclusion
  • orderBy - JSON string for sorting criteria
  • where - JSON string for filtering criteria

Example:

// URL: /users/paginated?page=2&pageSize=20&include={"profile":true}&orderBy={"name":"asc"}&where={"active":true}

const options = RequestOptionBuilder.buildSearchOptions(req, querySchemas);
// Result:
// {
// page: 2,
// pageSize: 20,
// include: { profile: true },
// orderBy: { name: 'asc' },
// where: { active: true }
// }

const result = await userRepository.findWithPagination(options);

Usage Examples​

Basic Controller Integration​

import { RequestOptionBuilder } from '@mifty/core/utils';
import { Request, Response } from 'express';

export class UserController extends BaseController<User, CreateUserDto, UpdateUserDto> {
constructor(userService: UserService) {
const querySchemas = RequestOptionBuilder.getDefaultQuerySchemas();

super({
service: userService,
responseClass: UserResponse,
querySchemas
});
}

// The BaseController automatically uses RequestOptionBuilder
// for getAll, findWithPagination, and other methods
}

Custom Schema Validation​

import { z } from 'zod';
import { RequestOptionBuilder } from '@mifty/core/utils';

export class UserController {
private querySchemas = {
...RequestOptionBuilder.getDefaultQuerySchemas(),
whereSchema: z.object({
active: z.boolean().optional(),
role: z.enum(['user', 'admin', 'moderator']).optional(),
email: z.object({
contains: z.string(),
mode: z.enum(['insensitive', 'default']).optional()
}).optional(),
createdAt: z.object({
gte: z.coerce.date().optional(),
lte: z.coerce.date().optional()
}).optional()
}).optional(),

includeSchema: z.object({
profile: z.boolean().optional(),
posts: z.union([
z.boolean(),
z.object({
where: z.object({
published: z.boolean().optional()
}).optional(),
orderBy: z.record(z.enum(['asc', 'desc'])).optional(),
take: z.number().int().positive().optional()
})
]).optional()
}).optional(),

orderBySchema: z.record(z.enum(['asc', 'desc'])).optional()
};

async getUsers(req: Request, res: Response) {
const options = RequestOptionBuilder.buildSearchOptions(req, this.querySchemas);
const result = await this.userService.findWithPagination(options);

return PaginatedResponse.paginated(
result.data,
result.page,
result.pageSize,
result.total
).send(res);
}
}

Manual Parameter Parsing​

import { RequestOptionBuilder } from '@mifty/core/utils';
import { z } from 'zod';

export class ProductController {
async searchProducts(req: Request, res: Response) {
// Parse individual parameters
const whereClause = req.query.where ?
RequestOptionBuilder.validateQueryParam(
req.query.where as string,
z.object({
category: z.string().optional(),
price: z.object({
gte: z.number().optional(),
lte: z.number().optional()
}).optional(),
inStock: z.boolean().optional()
})
) : {};

const orderBy = req.query.orderBy ?
RequestOptionBuilder.validateQueryParam(
req.query.orderBy as string,
z.record(z.enum(['asc', 'desc']))
) : { createdAt: 'desc' };

// Build search options manually
const searchOptions: SearchOptions = {
page: req.query.page ? parseInt(req.query.page as string) : 1,
pageSize: req.query.pageSize ? parseInt(req.query.pageSize as string) : 10,
where: whereClause,
orderBy,
include: { category: true, reviews: true }
};

const result = await this.productService.findWithPagination(searchOptions);
return PaginatedResponse.paginated(
result.data,
result.page,
result.pageSize,
result.total
).send(res);
}
}

URL Examples​

Basic Queries​

# Simple filtering
GET /users?where={"active":true}

# Sorting
GET /users?orderBy={"createdAt":"desc","name":"asc"}

# Including related data
GET /users?include={"profile":true,"posts":true}

# Pagination
GET /users/paginated?page=2&pageSize=25

Complex Queries​

# Date range filtering
GET /users?where={"createdAt":{"gte":"2024-01-01T00:00:00.000Z","lte":"2024-12-31T23:59:59.999Z"}}

# Text search with case insensitive matching
GET /users?where={"email":{"contains":"john","mode":"insensitive"}}

# Multiple conditions
GET /users?where={"OR":[{"name":{"contains":"john"}},{"email":{"contains":"john"}}],"active":true}

# Complex includes with nested conditions
GET /users?include={"profile":true,"posts":{"where":{"published":true},"orderBy":{"createdAt":"desc"},"take":5}}

Combined Parameters​

# Full query with all parameters
GET /users/paginated?page=1&pageSize=20&where={"active":true,"role":"user"}&orderBy={"createdAt":"desc"}&include={"profile":true}

Error Handling​

Validation Errors​

try {
const options = RequestOptionBuilder.buildSearchOptions(req, querySchemas);
const result = await this.userService.findWithPagination(options);
return PaginatedResponse.paginated(result.data, result.page, result.pageSize, result.total).send(res);
} catch (error) {
if (error instanceof BadRequestException) {
// Handle validation errors
return res.status(400).json({
success: false,
message: 'Invalid query parameters',
error: error.message
});
}
throw error;
}

Custom Error Messages​

const customSchemas = {
...RequestOptionBuilder.getDefaultQuerySchemas(),
paginationSchema: z.object({
page: z.number().int().positive().max(1000, 'Page number cannot exceed 1000'),
pageSize: z.number().int().positive().max(50, 'Page size cannot exceed 50')
}).strict()
};

Performance Considerations​

Query Optimization​

// Good: Limit includes to necessary data
const includeSchema = z.object({
profile: z.object({
select: z.object({
firstName: z.boolean().optional(),
lastName: z.boolean().optional(),
avatar: z.boolean().optional()
}).optional()
}).optional()
});

// Avoid: Including too much data
const includeSchema = z.record(z.any()); // Allows any includes

Pagination Limits​

const paginationSchema = z.object({
page: z.number().int().positive().max(10000),
pageSize: z.number().int().positive().max(100) // Reasonable limit
}).strict();

Security Considerations​

Input Validation​

// Good: Strict validation
const whereSchema = z.object({
active: z.boolean().optional(),
role: z.enum(['user', 'admin']).optional(),
email: z.string().email().optional()
}).strict(); // Reject unknown properties

// Avoid: Loose validation
const whereSchema = z.record(z.any()); // Allows any properties

SQL Injection Prevention​

The RequestOptionBuilder works with Prisma ORM, which provides built-in SQL injection protection through parameterized queries. However, always validate input structure:

// Good: Validate structure
const whereSchema = z.object({
name: z.object({
contains: z.string().max(100), // Limit length
mode: z.enum(['insensitive', 'default']).optional()
}).optional()
});

// Avoid: Raw SQL in where clauses (not possible with Prisma, but good practice)

Best Practices​

1. Use Specific Schemas​

// Good: Specific validation
const userQuerySchemas = {
whereSchema: z.object({
active: z.boolean().optional(),
role: z.enum(['user', 'admin', 'moderator']).optional(),
email: z.string().email().optional()
}).optional()
};

// Avoid: Generic validation
const querySchemas = {
whereSchema: z.record(z.any()).optional()
};

2. Set Reasonable Limits​

const paginationSchema = z.object({
page: z.number().int().positive().max(1000),
pageSize: z.number().int().positive().min(1).max(100)
}).strict();

3. Provide Default Values​

function buildSearchOptionsWithDefaults(req: Request, schemas: QuerySchemas): SearchOptions {
const options = RequestOptionBuilder.buildSearchOptions(req, schemas);

return {
page: 1,
pageSize: 10,
orderBy: { createdAt: 'desc' },
...options
};
}

4. Document Query Parameters​

/**
* Get users with pagination and filtering
*
* @param page - Page number (1-based, max 1000)
* @param pageSize - Items per page (1-100, default 10)
* @param where - Filter criteria: {"active": boolean, "role": "user"|"admin"}
* @param orderBy - Sort criteria: {"field": "asc"|"desc"}
* @param include - Related data: {"profile": boolean, "posts": boolean}
*/
async getUsers(req: Request, res: Response) {
// Implementation
}