// src/decorators/auto-crud.decorator.ts
import { SetMetadata } from '@nestjs/common';
export interface AutoCrudOptions {
apiPath?: string;
enableFilters?: boolean;
enablePagination?: boolean;
enableSorting?: boolean;
enablePopulate?: boolean;
excludeFields?: string[];
customValidation?: any;
}
export const AUTO_CRUD_KEY = 'auto-crud';
export const AutoCrud = (options: AutoCrudOptions = {}) => SetMetadata(AUTO_CRUD_KEY, options);
// src/interfaces/query.interface.ts
export interface FilterQuery {
[key: string]: any;
}
export interface PopulateQuery {
[key: string]: boolean | PopulateQuery;
}
export interface PaginationQuery {
page?: number;
limit?: number;
}
export interface SortQuery {
[key: string]: 'ASC' | 'DESC';
}
export interface BaseQueryDto {
filters?: FilterQuery;
locale?: string;
status?: string;
populate?: string | PopulateQuery;
fields?: string[];
sort?: string | SortQuery;
pagination?: PaginationQuery;
}
// src/dto/base-query.dto.ts
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, IsObject, IsArray, IsNumber, Min } from 'class-validator';
import { Transform, Type } from 'class-transformer';
export class BaseQueryDto {
@ApiPropertyOptional({
description: 'Filter the response',
example: { name: 'John', age: { $gte: 18 } }
})
@IsOptional()
@IsObject()
@Transform(({ value }) => typeof value === 'string' ? JSON.parse(value) : value)
filters?: any;
@ApiPropertyOptional({
description: 'Select a locale',
example: 'en'
})
@IsOptional()
@IsString()
locale?: string;
@ApiPropertyOptional({
description: 'Select the Draft & Publish status',
example: 'published'
})
@IsOptional()
@IsString()
status?: string;
@ApiPropertyOptional({
description: 'Populate relations, components, or dynamic zones',
example: 'user,category'
})
@IsOptional()
populate?: string;
@ApiPropertyOptional({
description: 'Select only specific fields to display',
example: ['name', 'email']
})
@IsOptional()
@IsArray()
@Transform(({ value }) => typeof value === 'string' ? value.split(',') : value)
fields?: string[];
@ApiPropertyOptional({
description: 'Sort the response',
example: 'name:asc,createdAt:desc'
})
@IsOptional()
sort?: string;
@ApiPropertyOptional({
description: 'Page number',
example: 1
})
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number;
@ApiPropertyOptional({
description: 'Number of items per page',
example: 10
})
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit?: number;
}
// src/services/base-crud.service.ts
import { Injectable } from '@nestjs/common';
import { Repository, SelectQueryBuilder, ObjectLiteral } from 'typeorm';
import { BaseQueryDto } from '../dto/base-query.dto';
@Injectable()
export class BaseCrudService<T extends ObjectLiteral> {
constructor(private readonly repository: Repository<T>) {}
async findAll(queryDto: BaseQueryDto): Promise<{ data: T[]; meta: any }> {
const queryBuilder = this.repository.createQueryBuilder('entity');
// Apply filters
if (queryDto.filters) {
this.applyFilters(queryBuilder, queryDto.filters);
}
// Apply populate/relations
if (queryDto.populate) {
this.applyPopulate(queryBuilder, queryDto.populate);
}
// Apply sorting
if (queryDto.sort) {
this.applySorting(queryBuilder, queryDto.sort);
}
// Apply field selection
if (queryDto.fields && queryDto.fields.length > 0) {
const fields = queryDto.fields.map(field => `entity.${field}`);
queryBuilder.select(fields);
}
// Apply pagination
const page = queryDto.page || 1;
const limit = queryDto.limit || 10;
const offset = (page - 1) * limit;
const totalCount = await queryBuilder.getCount();
const data = await queryBuilder.skip(offset).take(limit).getMany();
const meta = {
pagination: {
page,
pageSize: limit,
pageCount: Math.ceil(totalCount / limit),
total: totalCount
}
};
return { data, meta };
}
async findOne(id: string | number, populate?: string): Promise<T> {
const queryBuilder = this.repository.createQueryBuilder('entity');
queryBuilder.where('entity.id = :id', { id });
if (populate) {
this.applyPopulate(queryBuilder, populate);
}
return queryBuilder.getOne();
}
async create(createDto: Partial<T>): Promise<T> {
const entity = this.repository.create(createDto);
return this.repository.save(entity);
}
async update(id: string | number, updateDto: Partial<T>): Promise<T> {
await this.repository.update(id, updateDto);
return this.findOne(id);
}
async remove(id: string | number): Promise<void> {
await this.repository.delete(id);
}
private applyFilters(queryBuilder: SelectQueryBuilder<T>, filters: any): void {
Object.keys(filters).forEach((key, index) => {
const value = filters[key];
const paramKey = `filter_${key}_${index}`;
if (typeof value === 'object' && value !== null) {
// Handle operators like $gte, $lte, $like, etc.
Object.keys(value).forEach((operator, opIndex) => {
const opValue = value[operator];
const opParamKey = `${paramKey}_${opIndex}`;
switch (operator) {
case '$gte':
queryBuilder.andWhere(`entity.${key} >= :${opParamKey}`, { [opParamKey]: opValue });
break;
case '$lte':
queryBuilder.andWhere(`entity.${key} <= :${opParamKey}`, { [opParamKey]: opValue });
break;
case '$gt':
queryBuilder.andWhere(`entity.${key} > :${opParamKey}`, { [opParamKey]: opValue });
break;
case '$lt':
queryBuilder.andWhere(`entity.${key} < :${opParamKey}`, { [opParamKey]: opValue });
break;
case '$like':
queryBuilder.andWhere(`entity.${key} LIKE :${opParamKey}`, { [opParamKey]: `%${opValue}%` });
break;
case '$in':
queryBuilder.andWhere(`entity.${key} IN (:...${opParamKey})`, { [opParamKey]: opValue });
break;
case '$nin':
queryBuilder.andWhere(`entity.${key} NOT IN (:...${opParamKey})`, { [opParamKey]: opValue });
break;
default:
queryBuilder.andWhere(`entity.${key} = :${opParamKey}`, { [opParamKey]: opValue });
}
});
} else {
queryBuilder.andWhere(`entity.${key} = :${paramKey}`, { [paramKey]: value });
}
});
}
private applyPopulate(queryBuilder: SelectQueryBuilder<T>, populate: string): void {
const relations = populate.split(',').map(rel => rel.trim());
relations.forEach(relation => {
queryBuilder.leftJoinAndSelect(`entity.${relation}`, relation);
});
}
private applySorting(queryBuilder: SelectQueryBuilder<T>, sort: string): void {
const sortPairs = sort.split(',').map(s => s.trim());
sortPairs.forEach(pair => {
const [field, direction] = pair.split(':');
const sortDirection = direction?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
queryBuilder.addOrderBy(`entity.${field}`, sortDirection);
});
}
}
// src/controllers/base-crud.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
Query,
HttpCode,
HttpStatus
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiBody
} from '@nestjs/swagger';
import { BaseCrudService } from '../services/base-crud.service';
import { BaseQueryDto } from '../dto/base-query.dto';
export function createBaseCrudController<T>(
entityName: string,
createDto: any,
updateDto: any,
entityClass: any
) {
@ApiTags(entityName)
@Controller()
class BaseCrudController {
constructor(private readonly service: BaseCrudService<T>) {}
@Get()
@ApiOperation({ summary: `Get a list of ${entityName}` })
@ApiResponse({
status: 200,
description: `List of ${entityName} retrieved successfully`,
schema: {
type: 'object',
properties: {
data: {
type: 'array',
items: { $ref: `#/components/schemas/${entityClass.name}` }
},
meta: {
type: 'object',
properties: {
pagination: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
pageCount: { type: 'number' },
total: { type: 'number' }
}
}
}
}
}
}
})
async findAll(@Query() query: BaseQueryDto) {
return this.service.findAll(query);
}
@Get(':id')
@ApiOperation({ summary: `Get a ${entityName}` })
@ApiParam({ name: 'id', description: `${entityName} ID` })
@ApiResponse({
status: 200,
description: `${entityName} retrieved successfully`,
type: entityClass
})
async findOne(
@Param('id') id: string,
@Query('populate') populate?: string
) {
return this.service.findOne(id, populate);
}
@Post()
@ApiOperation({ summary: `Create a ${entityName}` })
@ApiBody({ type: createDto })
@ApiResponse({
status: 201,
description: `${entityName} created successfully`,
type: entityClass
})
async create(@Body() createEntityDto: any) {
return this.service.create(createEntityDto);
}
@Put(':id')
@ApiOperation({ summary: `Update a ${entityName}` })
@ApiParam({ name: 'id', description: `${entityName} ID` })
@ApiBody({ type: updateDto })
@ApiResponse({
status: 200,
description: `${entityName} updated successfully`,
type: entityClass
})
async update(
@Param('id') id: string,
@Body() updateEntityDto: any
) {
return this.service.update(id, updateEntityDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: `Delete a ${entityName}` })
@ApiParam({ name: 'id', description: `${entityName} ID` })
@ApiResponse({
status: 204,
description: `${entityName} deleted successfully`
})
async remove(@Param('id') id: string) {
return this.service.remove(id);
}
}
return BaseCrudController;
}
// src/utils/module-generator.ts
import { Module, DynamicModule, Type } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BaseCrudService } from '../services/base-crud.service';
import { createBaseCrudController } from '../controllers/base-crud.controller';
export interface CrudModuleOptions {
entity: any;
createDto: any;
updateDto: any;
apiPath?: string;
entityName?: string;
}
export function createCrudModule(options: CrudModuleOptions): DynamicModule {
const { entity, createDto, updateDto, apiPath, entityName } = options;
const serviceName = `${entity.name}CrudService`;
const controllerName = `${entity.name}CrudController`;
const CrudService = class extends BaseCrudService<any> {};
Object.defineProperty(CrudService, 'name', { value: serviceName });
const CrudController = createBaseCrudController(
entityName || entity.name.toLowerCase(),
createDto,
updateDto,
entity
);
Object.defineProperty(CrudController, 'name', { value: controllerName });
return {
module: class CrudModule {},
imports: [TypeOrmModule.forFeature([entity])],
controllers: [CrudController],
providers: [
{
provide: serviceName,
useFactory: (repository) => new CrudService(repository),
inject: [`${entity.name}Repository`]
}
],
exports: [serviceName]
};
}
// Example usage:
// src/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { Post } from './post.entity';
@Entity()
export class User {
@ApiProperty({ description: 'User ID' })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ description: 'User name' })
@Column()
name: string;
@ApiProperty({ description: 'User email' })
@Column({ unique: true })
email: string;
@ApiProperty({ description: 'User age' })
@Column()
age: number;
@ApiProperty({ description: 'User posts' })
@OneToMany(() => Post, post => post.user)
posts: Post[];
@ApiProperty({ description: 'Creation date' })
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
@ApiProperty({ description: 'Update date' })
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' })
updatedAt: Date;
}
// src/dto/create-user.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsEmail, IsNumber, Min } from 'class-validator';
export class CreateUserDto {
@ApiProperty({ description: 'User name', example: 'John Doe' })
@IsString()
name: string;
@ApiProperty({ description: 'User email', example: 'john@example.com' })
@IsEmail()
email: string;
@ApiProperty({ description: 'User age', example: 25 })
@IsNumber()
@Min(1)
age: number;
}
// src/dto/update-user.dto.ts
import { PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// src/modules/user.module.ts
import { Module } from '@nestjs/common';
import { createCrudModule } from '../utils/module-generator';
import { User } from '../entities/user.entity';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
@Module({})
export class UserModule {
static forRoot() {
return createCrudModule({
entity: User,
createDto: CreateUserDto,
updateDto: UpdateUserDto,
apiPath: 'users',
entityName: 'User'
});
}
}
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './modules/user.module';
import { User } from './entities/user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres', // or your database type
host: 'localhost',
port: 5432,
username: 'your_username',
password: 'your_password',
database: 'your_database',
entities: [User],
synchronize: true, // Don't use in production
}),
UserModule.forRoot(),
],
})
export class AppModule {}
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable validation
app.useGlobalPipes(new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}));
// Setup Swagger
const config = new DocumentBuilder()
.setTitle('Auto CRUD API')
.setDescription('Automatically generated CRUD API with advanced filtering')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);
await app.listen(3000);
console.log('Application is running on: http://localhost:3000');
console.log('Swagger documentation: http://localhost:3000/api/docs');
}
bootstrap();
/*
USAGE EXAMPLES:
1. Get all users with filters:
GET /api/users?filters={"age":{"$gte":18}}&sort=name:asc&page=1&limit=10
2. Get all users with population:
GET /api/users?populate=posts&fields=name,email
3. Create a user:
POST /api/users
{
"name": "John Doe",
"email": "john@example.com",
"age": 25
}
4. Update a user:
PUT /api/users/1
{
"name": "Jane Doe"
}
5. Delete a user:
DELETE /api/users/1
FEATURES:
- ✅ Automatic CRUD endpoints
- ✅ Advanced filtering with operators ($gte, $lte, $like, $in, etc.)
- ✅ Pagination support
- ✅ Sorting support
- ✅ Field selection
- ✅ Relation population
- ✅ Swagger documentation
- ✅ Validation with class-validator
- ✅ TypeScript support
- ✅ Extensible architecture
To add a new entity, simply:
1. Create the entity class
2. Create DTOs
3. Create a module using createCrudModule()
4. Import the module in AppModule
*/