Documentation Index
Fetch the complete documentation index at: https://mintlify.com/nestjsx/crud/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Soft delete allows you to mark records as deleted without permanently removing them from the database. This is useful for:
- Maintaining data history
- Implementing “trash” functionality
- Audit trails and compliance
- Recovering accidentally deleted records
Setup
Entity Configuration
Add a deletedAt column to your entity:
import { Entity, Column, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
name: string;
@DeleteDateColumn()
deletedAt?: Date;
}
The @DeleteDateColumn() decorator from TypeORM automatically handles soft delete timestamps.
Controller Configuration
Enable soft delete in your controller:
import { Controller } from '@nestjs/common';
import { Crud } from '@nestjsx/crud';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Crud({
model: { type: User },
query: {
softDelete: true,
},
})
@Controller('users')
export class UsersController {
constructor(public service: UsersService) {}
}
How It Works
When soft delete is enabled:
Delete Operation
Request:
Behavior:
- Sets
deletedAt to current timestamp
- Record remains in database
- Record is excluded from normal queries
Query Behavior
By default, soft-deleted records are excluded:
Request:
Result:
- Returns only records where
deletedAt IS NULL
- Soft-deleted records are hidden
Including Deleted Records
Use the includeDeleted query parameter:
Request:
GET /users?includeDeleted=1
Result:
- Returns all records, including soft-deleted ones
- Useful for showing “trash” views
Recovery Endpoint
The recoverOne endpoint restores soft-deleted records:
Request:
Behavior:
- Sets
deletedAt to null
- Record becomes visible in normal queries again
Control whether to return the recovered entity:
@Crud({
model: { type: User },
query: {
softDelete: true,
},
routes: {
recoverOneBase: {
returnRecovered: true, // Return the recovered entity
},
},
})
@Controller('users')
export class UsersController {
constructor(public service: UsersService) {}
}
Global Configuration
Enable soft delete globally for all controllers:
import { CrudConfigService } from '@nestjsx/crud';
CrudConfigService.load({
query: {
softDelete: true,
},
routes: {
recoverOneBase: {
returnRecovered: true,
},
},
});
Ensure all entities have a deletedAt column before enabling soft delete globally.
Complete Example
Here’s a full implementation:
Entity
import {
Entity,
Column,
PrimaryGeneratedColumn,
DeleteDateColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('companies')
export class Company {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
domain: string;
@Column({ type: 'text', nullable: true })
description?: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt?: Date;
}
Controller
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Crud } from '@nestjsx/crud';
import { Company } from './company.entity';
import { CompaniesService } from './companies.service';
@Crud({
model: { type: Company },
query: {
softDelete: true,
alwaysPaginate: false,
join: {
users: {
eager: true,
},
},
},
routes: {
deleteOneBase: {
returnDeleted: false, // Don't return entity after delete
},
recoverOneBase: {
returnRecovered: true, // Return entity after recovery
},
},
})
@ApiTags('companies')
@Controller('companies')
export class CompaniesController {
constructor(public service: CompaniesService) {}
}
Service
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);
}
}
Usage Examples
Soft Delete a Company
curl -X DELETE http://localhost:3000/companies/1
List Active Companies
curl http://localhost:3000/companies
Returns only companies where deletedAt IS NULL.
List All Companies (Including Deleted)
curl http://localhost:3000/companies?includeDeleted=1
Returns all companies, including soft-deleted ones.
Filter Deleted Companies Only
curl "http://localhost:3000/companies?includeDeleted=1&filter=deletedAt||$notnull"
Recover a Company
curl -X PATCH http://localhost:3000/companies/1/recover
With Relationships
Soft delete works with relationships:
@Crud({
model: { type: User },
query: {
softDelete: true,
join: {
company: {
eager: true,
},
projects: {},
},
},
})
@Controller('users')
export class UsersController {
constructor(public service: UsersService) {}
}
Related entities can also be soft-deleted. Configure each entity independently.
Cascade Soft Delete
To soft delete related entities, configure cascades in your entity:
import { Entity, OneToMany } from 'typeorm';
import { User } from '../users/user.entity';
@Entity('companies')
export class Company {
// ... other columns
@OneToMany(() => User, user => user.company, {
cascade: ['soft-remove'],
})
users: User[];
}
Disabling Specific Routes
You can disable the recover route if not needed:
@Crud({
model: { type: User },
query: {
softDelete: true,
},
routes: {
exclude: ['recoverOneBase'],
},
})
@Controller('users')
export class UsersController {
constructor(public service: UsersService) {}
}
Swagger Documentation
When soft delete is enabled, Swagger automatically documents:
includeDeleted query parameter on getMany and getOne
PATCH /:id/recover endpoint for recovery
deletedAt field in entity schemas
Hard Delete
To permanently delete records, implement a custom method:
import { Controller, Delete, Param } from '@nestjs/common';
import { Crud, Override } from '@nestjsx/crud';
@Crud({
model: { type: User },
query: {
softDelete: true,
},
})
@Controller('users')
export class UsersController {
constructor(public service: UsersService) {}
@Delete(':id/permanent')
async hardDelete(@Param('id') id: number) {
return this.service.hardDelete(id);
}
}
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService extends TypeOrmCrudService<User> {
constructor(
@InjectRepository(User)
private userRepo: Repository<User>
) {
super(userRepo);
}
async hardDelete(id: number) {
return this.userRepo.delete(id);
}
}
Best Practices
Use DeleteDateColumn
Always use TypeORM’s @DeleteDateColumn() decorator for consistency.
Index deletedAt
Add an index to deletedAt for better query performance.
Audit Trail
Combine with @CreateDateColumn() and @UpdateDateColumn() for complete audit trails.
Cleanup Strategy
Implement a cleanup job to hard delete old soft-deleted records if needed.
Database Migration
Add soft delete to existing tables:
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddSoftDeleteToUsers1234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'deletedAt',
type: 'timestamp',
isNullable: true,
default: null,
})
);
// Add index for performance
await queryRunner.query(
'CREATE INDEX "IDX_users_deletedAt" ON "users" ("deletedAt")'
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "IDX_users_deletedAt"');
await queryRunner.dropColumn('users', 'deletedAt');
}
}
Troubleshooting
Records Not Being Soft Deleted
Ensure:
- Entity has
@DeleteDateColumn() decorator
softDelete: true is set in query options
- Service extends
TypeOrmCrudService
Deleted Records Still Appearing
Check:
includeDeleted parameter isn’t being set
- Query filters aren’t overriding soft delete behavior
- Database column exists and is nullable