CRUD services extend the base service class to provide database operations. The @nestjsx/crud-typeorm package provides TypeOrmCrudService with built-in methods for all CRUD operations.
Basic service
A minimal CRUD service extends TypeOrmCrudService and injects a TypeORM repository:
import { Injectable } from '@nestjs/common' ;
import { InjectRepository } from '@nestjs/typeorm' ;
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm' ;
import { Company } from './company.entity' ;
@ Injectable ()
export class CompaniesService extends TypeOrmCrudService < Company > {
constructor (@ InjectRepository ( Company ) repo ) {
super ( repo );
}
}
From /home/daytona/workspace/source/integration/crud-typeorm/companies/companies.service.ts:1-13:
You must pass the injected repository to the super() constructor to enable all CRUD operations.
Service methods
The TypeOrmCrudService provides eight main methods that correspond to CRUD operations:
Read operations
getMany()
Retrieve multiple entities with filtering, pagination, sorting, and joins:
public async getMany ( req : CrudRequest ): Promise < GetManyDefaultResponse < T > | T [] >
Returns either an array of entities or a paginated response object:
interface GetManyDefaultResponse < T > {
data : T [];
count : number ;
total : number ;
page : number ;
pageCount : number ;
}
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:94-98:
getOne()
Retrieve a single entity by ID:
public async getOne ( req : CrudRequest ): Promise < T >
Throws NotFoundException if the entity doesn’t exist.
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:104-106:
Create operations
createOne()
Create a single entity:
public async createOne ( req : CrudRequest , dto : T | Partial < T > ): Promise < T >
By default, returns the full entity after creation. Set returnShallow: true in route options to return only the saved data without additional queries.
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:113-137:
createMany()
Create multiple entities in bulk:
public async createMany (
req : CrudRequest ,
dto : CreateManyDto < T | Partial < T >>
): Promise < T [] >
Entities are saved in chunks of 50 for optimal performance.
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:144-158:
Update operations
updateOne()
Partially update an entity (PATCH):
public async updateOne ( req : CrudRequest , dto : T | Partial < T > ): Promise < T >
Merges the DTO with the existing entity and saves. The allowParamsOverride option controls whether path parameters can be overwritten by the request body.
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:165-185:
replaceOne()
Fully replace an entity (PUT):
public async replaceOne ( req : CrudRequest , dto : T | Partial < T > ): Promise < T >
Replaces the entire entity with the DTO. Unlike updateOne(), this doesn’t merge with existing data.
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:202-231:
Delete operations
deleteOne()
Delete an entity (hard or soft delete):
public async deleteOne ( req : CrudRequest ): Promise < void | T >
Behavior depends on configuration:
If softDelete: true is set in query options, performs a soft delete
If returnDeleted: true is set in route options, returns the deleted entity
Otherwise, performs a hard delete and returns void
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:237-248:
recoverOne()
Recover a soft-deleted entity:
public async recoverOne ( req : CrudRequest ): Promise < T >
Only available when softDelete: true is enabled.
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:192-195:
Custom business logic
Extend the service to add custom methods or override existing ones:
import { Injectable } from '@nestjs/common' ;
import { InjectRepository } from '@nestjs/typeorm' ;
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm' ;
import { CrudRequest } from '@nestjsx/crud' ;
import { User } from './user.entity' ;
@ Injectable ()
export class UsersService extends TypeOrmCrudService < User > {
constructor (@ InjectRepository ( User ) repo ) {
super ( repo );
}
// Custom method
async findByEmail ( email : string ) : Promise < User > {
return this . repo . findOne ({ where: { email } });
}
// Override built-in method
async createOne ( req : CrudRequest , dto : Partial < User >) : Promise < User > {
// Hash password before saving
if ( dto . password ) {
dto . password = await this . hashPassword ( dto . password );
}
return super . createOne ( req , dto );
}
private async hashPassword ( password : string ) : Promise < string > {
// Password hashing logic
return password ; // Replace with actual hashing
}
}
Access the underlying TypeORM repository through this.repo for custom queries that go beyond CRUD operations.
Query builder
The service uses TypeORM’s query builder internally. You can access it through the createBuilder() method:
public async createBuilder (
parsed : ParsedRequestParams ,
options : CrudRequestOptions ,
many = true ,
withDeleted = false
): Promise < SelectQueryBuilder < T >>
This is useful for complex custom queries:
import { Injectable } from '@nestjs/common' ;
import { InjectRepository } from '@nestjs/typeorm' ;
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm' ;
import { Company } from './company.entity' ;
@ Injectable ()
export class CompaniesService extends TypeOrmCrudService < Company > {
constructor (@ InjectRepository ( Company ) repo ) {
super ( repo );
}
async findLargeCompanies ( minEmployees : number ) : Promise < Company []> {
const builder = this . repo . createQueryBuilder ( 'company' );
return builder
. leftJoin ( 'company.users' , 'user' )
. groupBy ( 'company.id' )
. having ( 'COUNT(user.id) >= :min' , { min: minEmployees })
. getMany ();
}
}
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:269-348:
Repository access
The service exposes common repository methods for convenience:
const service = new CompaniesService ( repo );
// Direct repository access
await service . findOne ({ where: { id: 1 } });
await service . find ({ take: 10 });
await service . count ({ where: { active: true } });
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:70-80:
public get findOne (): Repository < T > [ 'findOne' ] {
return this . repo . findOne . bind ( this . repo );
}
public get find (): Repository < T > [ 'find' ] {
return this . repo . find . bind ( this . repo );
}
public get count (): Repository < T > [ 'count' ] {
return this . repo . count . bind ( this . repo );
}
Soft deletes
Enable soft delete support in your entity and controller:
// Entity with soft delete
import { Entity , Column , DeleteDateColumn } from 'typeorm' ;
@ Entity ( 'companies' )
export class Company {
@ PrimaryGeneratedColumn ()
id : number ;
@ Column ()
name : string ;
@ DeleteDateColumn ({ nullable: true })
deletedAt ?: Date ;
}
// Controller configuration
@ Crud ({
model: { type: Company },
query: {
softDelete: true , // Enable soft delete
},
routes: {
deleteOneBase: {
returnDeleted: true , // Return deleted entity
},
},
})
@ Controller ( 'companies' )
export class CompaniesController {
constructor ( public service : CompaniesService ) {}
}
With soft delete enabled:
DELETE /companies/:id sets deletedAt instead of removing the row
Soft-deleted entities are excluded from queries by default
PATCH /companies/:id/recover restores soft-deleted entities
Use ?include_deleted=1 to include soft-deleted entities in results
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:314-318:
Validation groups
The service automatically applies validation groups based on the operation:
import { CrudValidationGroups } from '@nestjsx/crud' ;
import {
Entity ,
Column ,
PrimaryGeneratedColumn ,
} from 'typeorm' ;
import {
IsOptional ,
IsString ,
IsNotEmpty ,
IsEmpty ,
IsNumber ,
} from 'class-validator' ;
const { CREATE , UPDATE } = CrudValidationGroups ;
@ Entity ( 'companies' )
export class Company {
@ IsOptional ({ groups: [ UPDATE ] })
@ IsEmpty ({ groups: [ CREATE ] })
@ IsNumber ({}, { groups: [ UPDATE ] })
@ PrimaryGeneratedColumn ()
id ?: number ;
@ IsOptional ({ groups: [ UPDATE ] })
@ IsNotEmpty ({ groups: [ CREATE ] })
@ IsString ({ always: true })
@ Column ()
name : string ;
}
From /home/daytona/workspace/source/integration/crud-typeorm/companies/company.entity.ts:17-32:
The service applies:
CREATE group for createOne() and createMany()
UPDATE group for updateOne() and replaceOne()
Validation happens automatically through NestJS pipes. No additional configuration is needed in the service.
Security features
The service includes built-in SQL injection protection:
protected sqlInjectionRegEx : RegExp [] = [
/ ( %27 ) | ( \' ) | ( -- ) | ( %23 ) | ( # ) / gi ,
/ (( %3D ) | ( = )) [ ^ \n ] * (( %27 ) | ( \' ) | ( -- ) | ( %3B ) | ( ; )) / gi ,
/w * (( %27 ) | ( \' ' ))(( %6F ) | o | ( %4F ))(( %72 ) | r | ( %52 )) / gi ,
/ (( %27 ) | ( \' )) union/ gi ,
];
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:56-61:
All query parameters are validated against these patterns before being executed.
Complete example
Here’s a complete service with custom logic:
import { Injectable } from '@nestjs/common' ;
import { InjectRepository } from '@nestjs/typeorm' ;
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm' ;
import { CrudRequest } from '@nestjsx/crud' ;
import { User } from './user.entity' ;
@ Injectable ()
export class UsersService extends TypeOrmCrudService < User > {
constructor (@ InjectRepository ( User ) repo ) {
super ( repo );
}
// Custom query with joins
async findActiveUsers () : Promise < User []> {
return this . repo
. createQueryBuilder ( 'user' )
. leftJoinAndSelect ( 'user.company' , 'company' )
. where ( 'user.isActive = :active' , { active: true })
. getMany ();
}
// Override create to add custom logic
async createOne ( req : CrudRequest , dto : Partial < User >) : Promise < User > {
// Normalize email
if ( dto . email ) {
dto . email = dto . email . toLowerCase (). trim ();
}
// Call base implementation
return super . createOne ( req , dto );
}
// Override delete for audit logging
async deleteOne ( req : CrudRequest ) : Promise < void | User > {
const user = await this . getOne ( req );
console . log ( `Deleting user: ${ user . email } ` );
return super . deleteOne ( req );
}
}
From /home/daytona/workspace/source/integration/crud-typeorm/users/users.service.ts:1-13:
Next steps
Controllers Learn how to configure CRUD controllers
Requests Understand request parsing and filtering