npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@foryourdev/nestjs-crud

v0.3.21

Published

Automatically generate CRUD Rest API based on NestJS and TypeOrm

Readme

nestjs-crud

npm version License: MIT

A powerful library that automatically generates RESTful CRUD APIs based on NestJS and TypeORM.

📋 Table of Contents

✨ Features

🚀 Core Features

  • Automatic CRUD Route Generation: Auto-generate APIs based on TypeORM entities
  • RESTful Standard Compliance: API endpoints following industry standards
  • Automatic Swagger Generation: Auto-generate and maintain API documentation
  • Powerful Validation: Data validation through class-validator
  • Full TypeScript Support: Type safety and IntelliSense support

🔍 Advanced Query Features

  • Filtering: Support for 30+ filter operators
  • Sorting: Multi-field sorting support
  • Relation Inclusion: Relationship data loading with nested relation support
  • Pagination: Support for Offset, Cursor, and Number-based pagination
  • Search: Complex search condition support

🛠 Database Features

  • Soft Delete: Mark data as deleted without actual deletion
  • Recovery: Recover soft-deleted data
  • Upsert: Update if exists, create if doesn't exist
  • Lifecycle Hooks: Execute custom logic at each stage of CRUD operations
    • Decorator Approach 🆕: Intuitive method decorators like @BeforeCreate(), @AfterUpdate(), @BeforeDestroy(), @BeforeRecover()
    • Routes Configuration Approach: Legacy routes.hooks configuration approach
  • Full-Text Search 🆕: PostgreSQL FTS support with GIN indexes and plainto_tsquery

🔒 Security and Control Features

  • Filter Restrictions: Only columns specified in allowedFilters can be filtered
  • Parameter Restrictions: Only columns specified in allowedParams can be used as request parameters
  • Relation Inclusion Restrictions: Only relations specified in allowedIncludes can be included
  • Default Block Policy: If not configured, all filtering/parameters/relation inclusion is blocked
  • Field Exclusion 🆕: Global and route-specific field exclusion from responses

⚡ Performance Features (v0.2.7+)

  • Built-in Caching: Memory, Redis, and multi-tier caching strategies
  • Lazy Loading: Load relations only when needed
  • Auto Relation Detection: Automatic N+1 query optimization
  • Enhanced Services: CacheableCrudService and EnhancedCrudService for advanced features

📦 Installation

npm install nestjs-crud
# or
yarn add nestjs-crud

Required Dependencies

npm install @nestjs/common @nestjs/core typeorm class-validator class-transformer

🚀 Quick Start

1. Create Entity

// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { IsString, IsEmail, IsOptional } from 'class-validator';

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    @IsString()
    name: string;

    @Column({ unique: true })
    @IsEmail()
    email: string;

    @Column({ nullable: true })
    @IsOptional()
    @IsString()
    bio?: string;

    @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
    createdAt: Date;

    @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
    updatedAt: Date;
}

2. Create Service

// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CrudService } from 'nestjs-crud';
import { User } from './user.entity';

@Injectable()
export class UserService extends CrudService<User> {
    constructor(
        @InjectRepository(User)
        repository: Repository<User>,
    ) {
        super(repository);
    }
}

3. Create Controller

// user.controller.ts
import { Controller } from '@nestjs/common';
import { Crud, BeforeCreate } from 'nestjs-crud';
import { UserService } from './user.service';
import { User } from './user.entity';
import * as bcrypt from 'bcrypt';

@Controller('users')
@Crud({
    entity: User,
})
export class UserController {
    constructor(public readonly crudService: UserService) {}

    // 🆕 NEW! Add logic easily with lifecycle hook decorators
    @BeforeCreate()
    async hashPassword(body: any, context: any) {
        if (body.password) {
            body.password = await bcrypt.hash(body.password, 10);
        }
        return body;
    }
}

4. Module Configuration

// user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { User } from './user.entity';

@Module({
    imports: [TypeOrmModule.forFeature([User])],
    controllers: [UserController],
    providers: [UserService],
})
export class UserModule {}

🎯 Basic CRUD Operations

The above configuration automatically generates the following API endpoints:

| HTTP Method | Endpoint | Description | Method Name | | ----------- | -------------------- | ----------------------- | ----------- | | GET | /users | Get list of users | index | | GET | /users/:id | Get specific user | show | | POST | /users | Create new user | create | | PUT | /users/:id | Update user information | update | | DELETE | /users/:id | Delete user | destroy | | POST | /users/upsert | Create or update user | upsert | | POST | /users/:id/recover | Recover deleted user | recover |

📊 Unified Response Structure

All CRUD operations provide a consistent response structure with metadata:

GET /users (index) - Pagination Response

{
    "data": [
        { "id": 1, "name": "John Doe", "email": "[email protected]" },
        { "id": 2, "name": "Jane Smith", "email": "[email protected]" },
        { "id": 3, "name": "Bob Johnson", "email": "[email protected]" }
    ],
    "metadata": {
        "operation": "index",
        "timestamp": "2024-01-15T11:00:00.000Z",
        "affectedCount": 3,
        "includedRelations": ["department", "posts"],
        "pagination": {
            "type": "offset",
            "total": 150,
            "page": 1,
            "pages": 15,
            "offset": 10,
            "nextCursor": "eyJpZCI6M30="
        }
    }
}

GET /users (cursor pagination)

{
    "data": [
        { "id": 4, "name": "Alice Brown", "email": "[email protected]" },
        { "id": 5, "name": "Charlie Wilson", "email": "[email protected]" }
    ],
    "metadata": {
        "operation": "index",
        "timestamp": "2024-01-15T11:00:00.000Z",
        "affectedCount": 2,
        "pagination": {
            "type": "cursor",
            "total": 150,
            "limit": 2,
            "totalPages": 75,
            "nextCursor": "eyJpZCI6NX0="
        }
    }
}

GET /users/:id (show)

{
    "data": {
        "id": 1,
        "name": "John Doe",
        "email": "[email protected]",
        "createdAt": "2024-01-15T10:30:00.000Z"
    },
    "metadata": {
        "operation": "show",
        "timestamp": "2024-01-15T11:00:00.000Z",
        "affectedCount": 1,
        "includedRelations": ["department"],
        "excludedFields": ["password"]
    }
}

POST /users (create)

{
    "data": {
        "id": 1,
        "name": "John Doe",
        "email": "[email protected]",
        "createdAt": "2024-01-15T10:30:00.000Z"
    },
    "metadata": {
        "operation": "create",
        "timestamp": "2024-01-15T10:30:00.000Z",
        "affectedCount": 1
    }
}

PUT /users/:id (update)

{
    "data": {
        "id": 1,
        "name": "John Doe Updated",
        "email": "[email protected]",
        "updatedAt": "2024-01-15T11:00:00.000Z"
    },
    "metadata": {
        "operation": "update",
        "timestamp": "2024-01-15T11:00:00.000Z",
        "affectedCount": 1
    }
}

POST /users/upsert (upsert)

{
    "data": {
        "id": 1,
        "name": "John Doe Upsert",
        "email": "[email protected]"
    },
    "metadata": {
        "operation": "upsert",
        "timestamp": "2024-01-15T11:00:00.000Z",
        "affectedCount": 1,
        "isNew": false // true: newly created, false: existing data updated
    }
}

DELETE /users/:id (destroy)

{
    "data": {
        "id": 1,
        "name": "John Doe",
        "email": "[email protected]",
        "deletedAt": "2024-01-15T11:00:00.000Z"
    },
    "metadata": {
        "operation": "destroy",
        "timestamp": "2024-01-15T11:00:00.000Z",
        "affectedCount": 1,
        "wasSoftDeleted": true // true: soft delete, false: hard delete
    }
}

POST /users/:id/recover (recover)

{
    "data": {
        "id": 1,
        "name": "John Doe",
        "email": "[email protected]",
        "deletedAt": null
    },
    "metadata": {
        "operation": "recover",
        "timestamp": "2024-01-15T11:00:00.000Z",
        "affectedCount": 1,
        "wasSoftDeleted": true // Whether it was soft deleted before recovery
    }
}

Multiple Creation (POST /users - array submission)

{
    "data": [
        { "id": 1, "name": "John Doe", "email": "[email protected]" },
        { "id": 2, "name": "Jane Smith", "email": "[email protected]" }
    ],
    "metadata": {
        "operation": "create",
        "timestamp": "2024-01-15T10:30:00.000Z",
        "affectedCount": 2
    }
}

📝 Bulk Operations Support

| Operation | Single | Bulk (Multiple) | Description | |-----------|--------|-----------------|-------------| | CREATE | ✅ Supported | ✅ Supported | Submit single object or array of objects | | UPDATE | ✅ Supported | ✅ Supported | Submit array with ID for each item | | UPSERT | ✅ Supported | ✅ Supported | Submit array of objects for bulk upsert | | DELETE | ✅ Supported | ✅ Supported | Submit array of IDs or conditions | | RECOVER | ✅ Supported | ✅ Supported | Submit array of IDs for bulk recovery |

Bulk Create Example

# Single creation
POST /users
{
    "name": "John Doe",
    "email": "[email protected]"
}

# Bulk creation (array submission)
POST /users
[
    { "name": "John Doe", "email": "[email protected]" },
    { "name": "Jane Smith", "email": "[email protected]" },
    { "name": "Bob Johnson", "email": "[email protected]" }
]

Bulk Update Example (NEW! 🆕)

# Single update
PUT /users/1
{
    "name": "John Updated",
    "email": "[email protected]"
}

# Bulk update (array submission with IDs)
PUT /users
[
    { "id": 1, "name": "John Updated", "email": "[email protected]" },
    { "id": 2, "name": "Jane Updated", "email": "[email protected]" },
    { "id": 3, "name": "Bob Updated", "email": "[email protected]" }
]

Bulk Upsert Example (NEW! 🆕)

# Single upsert
POST /users/upsert
{
    "id": 1,
    "name": "John Doe",
    "email": "[email protected]"
}

# Bulk upsert (array submission)
POST /users/upsert
[
    { "id": 1, "name": "John Doe", "email": "[email protected]" },  // Update if exists
    { "name": "New User", "email": "[email protected]" },             // Create if not exists
    { "id": 99, "name": "Another User", "email": "[email protected]" }
]

Bulk Delete Example (NEW! 🆕)

# Single delete
DELETE /users/1

# Bulk delete (array of IDs in query params)
DELETE /users?ids=1,2,3

# Or via request body
DELETE /users
{
    "ids": [1, 2, 3]
}

Bulk Recover Example (NEW! 🆕)

# Single recover
POST /users/1/recover

# Bulk recover (array of IDs)
POST /users/recover
{
    "ids": [1, 2, 3]
}

Note: All bulk operations support lifecycle hooks and will execute them for each item in the batch.

🔍 RESTful Query Parameters

📋 Filtering

⚠️ Important: Query Parameter Format

nestjs-crud uses underscore separator format. MongoDB-style $ operators are not supported.

# ✅ Correct format (underscore separator)
GET /users?filter[email_eq][email protected]
GET /users?filter[age_gte]=18
GET /users?filter[name_like]=%John%

# ❌ Unsupported format (MongoDB style)
GET /users?filter[email][$eq][email protected]     # Won't work
GET /users?filter[age][$gte]=18                    # Won't work
GET /users?filter[name][$like]=%John%              # Won't work

Parsing method:

  • filter[field_operator]=value → ✅ Works correctly
  • filter[field][$operator]=value → ❌ Filter is ignored

Basic Comparison Operators

# Equals
GET /users?filter[name_eq]=John Doe
GET /users?filter[age_eq]=25

# Not equals
GET /users?filter[status_ne]=inactive
GET /users?filter[role_ne]=admin

Size Comparison Operators

# Greater than/Greater than or equal
GET /users?filter[age_gt]=18
GET /users?filter[age_gte]=18

# Less than/Less than or equal
GET /users?filter[age_lt]=65
GET /users?filter[age_lte]=65

# Range
GET /users?filter[age_between]=18,65
GET /users?filter[salary_between]=30000,80000

String Pattern Operators

# LIKE pattern (case sensitive)
GET /users?filter[name_like]=%John%
GET /users?filter[email_like]=%@gmail.com

# ILIKE pattern (case insensitive)
GET /users?filter[name_ilike]=%JOHN%
GET /users?filter[email_ilike]=%GMAIL%

# Start/End patterns
GET /users?filter[name_start]=John
GET /users?filter[email_end]=.com

# Contains
GET /users?filter[bio_contains]=developer

Array/List Operators

# Include (IN)
GET /users?filter[id_in]=1,2,3,4,5
GET /users?filter[role_in]=admin,manager,user

# Exclude (NOT IN)
GET /users?filter[status_not_in]=deleted,banned
GET /users?filter[role_not_in]=guest

NULL/Existence Check Operators

# NULL check
GET /users?filter[deleted_at_null]=true
GET /users?filter[last_login_null]=true

# NOT NULL check
GET /users?filter[avatar_not_null]=true
GET /users?filter[email_verified_at_not_null]=true

# Existence check (not null and not empty string)
GET /users?filter[bio_present]=true

# Blank check (null or empty string)
GET /users?filter[middle_name_blank]=true

Relation Filtering

# Nested relation filtering
GET /posts?filter[author.name_like]=%John%
GET /posts?filter[author.department.name_eq]=Development
GET /comments?filter[post.author.role_eq]=admin

🔄 Sorting

# Single field sorting
GET /users?sort=name                    # Name ascending
GET /users?sort=-created_at             # Creation date descending

# Multi-field sorting
GET /users?sort=role,name,-created_at   # Role>Name>Creation date order

# Relation field sorting
GET /posts?sort=author.name,-created_at
GET /users?sort=department.name,name

🔗 Including Relations

⚠️ Important Changes:

  • The routes.relations option has been deprecated.
  • Now use allowedIncludes configuration and include query parameter together.
  • Enhanced Security: If allowedIncludes is not configured, all relation inclusion is blocked.
# Single relation (only relations allowed in allowedIncludes)
GET /users?include=department
GET /posts?include=author

# Multiple relations
GET /users?include=department,posts
GET /posts?include=author,comments

# Nested relations
GET /posts?include=author,comments.author
GET /users?include=department.company,posts.comments
GET /orders?include=customer.address,items.product.category

Before and After Comparison

// ❌ Previous approach (deprecated)
@Crud({
  entity: User,
  routes: {
    index: {
      relations: ['department', 'posts'], // Relations included by default
    }
  }
})

// ✅ New approach (enhanced security)
@Crud({
  entity: User,
  allowedIncludes: ['department', 'posts'], // Explicitly specify allowed relations
  routes: {
    index: {
      allowedIncludes: ['department', 'posts', 'posts.comments'], // Additional method-specific allowances
    }
  }
})

// When relations are needed, explicitly request via query parameter
GET /users?include=department,posts

Security Policy

// 1. allowedIncludes not configured → All relations blocked
@Crud({
  entity: User,
  // No allowedIncludes → All includes ignored
})

// 2. Global configuration
@Crud({
  entity: User,
  allowedIncludes: ['department'], // Only department allowed
})

// 3. Method-specific configuration (higher priority)
@Crud({
  entity: User,
  allowedIncludes: ['department'], // Global: department only
  routes: {
    index: {
      allowedIncludes: ['department', 'posts'], // INDEX allows posts additionally
    },
    show: {
      // No allowedIncludes → Uses global configuration: department only
    },
  },
})

Benefits

  1. Enhanced Security: Only explicitly allowed relations can be included
  2. Explicit Requests: Selectively load only necessary relations
  3. Performance Optimization: Prevent unnecessary relation loading
  4. N+1 Problem Prevention: Handle necessary relations with JOINs only
  5. Fine-grained Control: Apply different relation inclusion policies per method

📄 Pagination

Page Number Method

GET /users?page[number]=1&page[size]=10     # Page 1, 10 items per page
GET /users?page[number]=3&page[size]=20     # Page 3, 20 items per page

Offset Method

GET /users?page[offset]=0&page[limit]=10    # First 10 items
GET /users?page[offset]=20&page[limit]=10   # 10 items starting from 20th

Cursor Method

GET /users?page[cursor]=eyJpZCI6MTB9&page[size]=10

📊 Pagination Response Structure

Offset/Number Pagination Response

{
    "data": [
        { "id": 1, "name": "John Doe", "email": "[email protected]" },
        { "id": 2, "name": "Jane Smith", "email": "[email protected]" }
    ],
    "metadata": {
        "page": 1, // Current page number
        "pages": 10, // Total pages ✅
        "total": 95, // Total data count
        "offset": 10, // Next offset
        "nextCursor": "..." // Next page token
    }
}

Cursor Pagination Response

{
    "data": [
        { "id": 1, "name": "John Doe", "email": "[email protected]" },
        { "id": 2, "name": "Jane Smith", "email": "[email protected]" }
    ],
    "metadata": {
        "total": 95, // Total data count
        "totalPages": 10, // Total pages ✅
        "limit": 10, // Page size
        "nextCursor": "..." // Next page token
    }
}

🔍 Complex Query Examples

Check out complex query usage through real-world use cases:

User Search Example

# Get 10 active adult users sorted by recent registration
GET /users?filter[status_eq]=active&
          filter[age_gte]=18&
          sort=-created_at&
          page[number]=1&page[size]=10

Post Search Example

# Get published posts by specific author with author information
GET /posts?filter[author.name_like]=%John%&
          filter[status_eq]=published&
          filter[created_at_gte]=2024-01-01&
          include=author,comments&
          sort=-created_at,title&
          page[number]=1&page[size]=20

Order Search Example

# Get completed orders with customer and order item information
GET /orders?filter[status_eq]=completed&
           filter[total_amount_gte]=50000&
           filter[created_at_between]=2024-01-01,2024-12-31&
           include=customer.address,items.product&
           sort=-created_at&
           page[offset]=0&page[limit]=50

⚙️ Advanced Configuration

🔒 Security Control Settings

Filter Restrictions - allowedFilters

@Controller('users')
@Crud({
    entity: User,
    allowedFilters: ['name', 'email', 'status'], // Global: only these columns allowed for filtering
    routes: {
        index: {
            allowedFilters: ['name', 'email', 'status', 'createdAt'], // INDEX allows more columns
        },
        show: {
            allowedFilters: ['name'], // SHOW allows only name
        },
    },
})
export class UserController {
    constructor(public readonly crudService: UserService) {}
}

Operation examples:

# ✅ Allowed columns - works normally
GET /users?filter[name_like]=%John%
GET /users?filter[email_eq][email protected]

# ❌ Disallowed columns - filter ignored
GET /users?filter[password_eq]=secret  # Ignored if password not in allowedFilters

Parameter Restrictions - allowedParams

@Controller('users')
@Crud({
    entity: User,
    allowedParams: ['name', 'email'], // Global: only these columns allowed as request parameters
    routes: {
        create: {
            allowedParams: ['name', 'email', 'status'], // CREATE allows additional status
        },
        update: {
            allowedParams: ['name'], // UPDATE allows only name
        },
        upsert: {
            // No allowedParams -> uses global configuration: name, email only
        },
    },
})
export class UserController {
    constructor(public readonly crudService: UserService) {}
}

Operation examples:

// Configuration: allowedParams: ['name', 'email']

// ✅ Only allowed parameters are processed
POST /users
{
  "name": "John Doe",        // ✅ Processed
  "email": "[email protected]",  // ✅ Processed
  "password": "secret",      // ❌ Removed (not in allowedParams)
  "internal_id": 123         // ❌ Removed (not in allowedParams)
}

// Actually processed data:
{
  "name": "John Doe",
  "email": "[email protected]"
}

Relation Inclusion Restrictions - allowedIncludes

@Controller('posts')
@Crud({
    entity: Post,
    allowedIncludes: ['author'], // Global: only author relation allowed
    routes: {
        index: {
            allowedIncludes: ['author', 'comments', 'tags'], // INDEX allows more relations
        },
        show: {
            allowedIncludes: ['author', 'comments.author'], // SHOW allows nested relations
        },
    },
})
export class PostController {
    constructor(public readonly crudService: PostService) {}
}

Operation examples:

# ✅ Only allowed relations are included
GET /posts?include=author           # ✅ Included
GET /posts?include=comments         # ✅ Included (in INDEX)
GET /posts?include=author,comments  # ✅ Both included

# ❌ Disallowed relations are ignored
GET /posts?include=author,likes,comments  # ✅ Only author,comments included (likes ignored)
GET /posts?include=profile               # ❌ All relations ignored (profile not allowed)

🎛️ CRUD Options Configuration

@Controller('users')
@Crud({
    entity: User,
    only: ['index', 'show', 'create', 'update'], // Enable only specific methods
    allowedFilters: ['name', 'email', 'status'], // Allowed filter columns
    allowedParams: ['name', 'email', 'bio'], // Allowed request parameters
    allowedIncludes: ['department', 'posts'], // Allowed relation inclusions
    routes: {
        index: {
            paginationType: PaginationType.OFFSET,
            numberOfTake: 20,
            sort: Sort.DESC,
            softDelete: false,
            allowedFilters: ['name', 'email', 'status', 'createdAt'], // Method-specific filter settings
            allowedIncludes: ['department', 'posts', 'posts.comments'], // Method-specific relation settings
        },
        show: {
            softDelete: true,
            allowedFilters: ['name', 'email'], // SHOW has restrictive filtering
            allowedIncludes: ['department'], // SHOW has basic relations only
        },
        create: {
            hooks: {
                assignBefore: async (body, context) => {
                    // Email normalization
                    if (body.email) {
                        body.email = body.email.toLowerCase().trim();
                    }
                    return body;
                },
                saveAfter: async (entity, context) => {
                    // Send user creation event
                    await eventBus.publish('user.created', entity);
                    return entity;
                },
            },
        },
        update: {
            hooks: {
                assignBefore: async (body, context) => {
                    body.updatedAt = new Date();
                    return body;
                },
            },
        },
        destroy: {
            softDelete: true,
        },
    },
})
export class UserController {
    constructor(public readonly crudService: UserService) {}
}

🔄 Lifecycle Hooks

Execute custom logic at each stage of CRUD operations through lifecycle hooks.

Hook Types

| Hook | Execution Point | Purpose | Supported Routes | | --------------- | -------------------------- | -------------------------------- | ----------------------------- | | assignBefore | Before data assignment | Input validation, transformation | create, update, upsert, show* | | assignAfter | After data assignment | Entity post-processing | create, update, upsert, show* | | saveBefore | Before saving | Final validation, business logic | create, update, upsert | | saveAfter | After saving | Notifications, event generation | create, update, upsert | | destroyBefore | Before entity deletion | Permission check, cleanup prep | destroy | | destroyAfter | After entity deletion | Audit logs, external sync | destroy | | recoverBefore | Before entity recovery | Permission check, recovery prep | recover | | recoverAfter | After entity recovery | Audit logs, notifications | recover |

Note: For show operation, assignBefore processes query parameters before entity lookup, and assignAfter processes the retrieved entity before returning it.

🎯 Method 1: Decorator Approach (NEW! 🆕 Recommended)

Use decorators on class methods for intuitive usage.

Available Decorators

Basic decorators:

@BeforeCreate()  // Execute before CREATE (assignBefore)
@AfterCreate()   // Execute after CREATE (saveAfter)
@BeforeUpdate()  // Execute before UPDATE (assignBefore)
@AfterUpdate()   // Execute after UPDATE (saveAfter)
@BeforeUpsert()  // Execute before UPSERT (assignBefore)
@AfterUpsert()   // Execute after UPSERT (saveAfter)
@BeforeDestroy() // Execute before DESTROY (destroyBefore)
@AfterDestroy()  // Execute after DESTROY (destroyAfter)
@BeforeRecover() // Execute before RECOVER (recoverBefore)
@AfterRecover()  // Execute after RECOVER (recoverAfter)
@BeforeShow()    // Execute before SHOW (assignBefore for show)
@AfterShow()     // Execute after SHOW (assignAfter for show)

Consistent fine-grained control decorators:

@BeforeAssign('create' | 'update' | 'upsert' | 'show')  // Before assignment
@AfterAssign('create' | 'update' | 'upsert' | 'show')   // After assignment
@BeforeSave('create' | 'update' | 'upsert')    // Before saving
@AfterSave('create' | 'update' | 'upsert')     // After saving
@BeforeDestroy('destroy')                       // Before entity deletion
@AfterDestroy('destroy')                        // After entity deletion
@BeforeRecover('recover')                       // Before entity recovery
@AfterRecover('recover')                        // After entity recovery

🆕 New 4-stage detailed decorators (clearer control):

// === ASSIGN stage (data assignment to entity) ===
@BeforeAssignCreate()  @BeforeAssignUpdate()  @BeforeAssignUpsert()  @BeforeAssignShow()  // Before assignment
@AfterAssignCreate()   @AfterAssignUpdate()   @AfterAssignUpsert()   @AfterAssignShow()   // After assignment

// === SAVE stage (database saving) ===
@BeforeSaveCreate()    @BeforeSaveUpdate()    @BeforeSaveUpsert()    // Before saving
@AfterSaveCreate()     @AfterSaveUpdate()     @AfterSaveUpsert()     // After saving

// === DESTROY stage (entity deletion) ===
@BeforeDestroyDestroy()  // Before entity deletion
@AfterDestroyDestroy()   // After entity deletion

// === RECOVER stage (entity recovery) ===
@BeforeRecoverRecover()  // Before entity recovery
@AfterRecoverRecover()   // After entity recovery
Real Usage Examples

Using basic decorators:

import { Controller, Post, Put } from '@nestjs/common';
import { Crud, BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate } from 'nestjs-crud';
import { User } from './user.entity';
import { UserService } from './user.service';
import * as bcrypt from 'bcrypt';

@Crud({
    entity: User,
    allowedParams: ['name', 'email', 'password', 'phone'],
})
@Controller('users')
export class UserController {
    constructor(public readonly crudService: UserService) {}

    // 🔐 Hash password before CREATE
    @BeforeCreate()
    async hashPasswordOnCreate(body: any, context: any) {
        if (body.password) {
            console.log('CREATE: Hashing password...');
            body.password = await bcrypt.hash(body.password, 10);
        }

        // Set default values
        body.provider = body.provider || 'local';
        body.role = body.role || 'user';

        return body;
    }

    // 📧 Send welcome email after CREATE
    @AfterCreate()
    async sendWelcomeEmail(entity: User, context: any) {
        console.log(`New user created: ${entity.email} (ID: ${entity.id})`);

        // Welcome email sending logic
        // await this.emailService.sendWelcomeEmail(entity);

        return entity;
    }

    // 🔐 Hash password before UPDATE too
    @BeforeUpdate()
    async hashPasswordOnUpdate(body: any, context: any) {
        if (body.password) {
            console.log('UPDATE: Hashing password...');
            body.password = await bcrypt.hash(body.password, 10);
        }

        // Auto-set update time
        body.updatedAt = new Date();

        return body;
    }

    // 📝 Log user update after UPDATE
    @AfterUpdate()
    async logUserUpdate(entity: User, context: any) {
        console.log(`User update completed: ${entity.email} (ID: ${entity.id})`);

        // Record update log
        // await this.auditService.logUserUpdate(entity, context.request?.user);

        return entity;
    }

    // 🗑️ Permission check and cleanup preparation before DELETE
    @BeforeDestroy()
    async beforeDeleteUser(entity: User, context: any) {
        console.log(`DELETE permission check for: ${entity.email} (ID: ${entity.id})`);

        // Check delete permission
        const userId = context.request?.user?.id;
        if (entity.id !== userId) {
            const userRole = context.request?.user?.role;
            if (userRole !== 'admin') {
                throw new Error('Permission denied: Cannot delete other users');
            }
        }

        // Prevent deletion of locked users
        if (entity.status === 'locked') {
            throw new Error('Cannot delete locked user');
        }

        // Set deletion metadata
        entity.deletedBy = userId;
        entity.deletedAt = new Date();

        return entity;
    }

    // 🧹 Cleanup and notification after DELETE
    @AfterDestroy()
    async afterDeleteUser(entity: User, context: any) {
        console.log(`User deleted successfully: ${entity.email} (ID: ${entity.id})`);

        // Cleanup related data
        // await this.sessionService.revokeUserSessions(entity.id);
        // await this.tokenService.revokeUserTokens(entity.id);

        // Send deletion notification
        // await this.notificationService.notifyUserDeletion(entity);

        // Log deletion for audit
        // await this.auditService.logUserDeletion(entity, context.request?.user);

        return entity;
    }

    // 🚀 NEW! Permission check and preparation before RECOVER
    @BeforeRecover()
    async beforeRecoverUser(entity: User, context: HookContext<User>) {
        console.log(`RECOVER permission check for: ${entity.email} (ID: ${entity.id})`);

        // Check recover permission
        const userId = context.request?.user?.id;
        if (entity.deletedBy !== userId) {
            const userRole = context.request?.user?.role;
            if (userRole !== 'admin') {
                throw new Error('Permission denied: Cannot recover other users data');
            }
        }

        // Prevent recovery of certain users
        if (entity.status === 'banned') {
            throw new Error('Cannot recover banned user');
        }

        // Set recovery metadata
        entity.recoveredBy = userId;
        entity.recoveredAt = new Date();

        return entity;
    }

    // 🚀 NEW! Cleanup and notification after RECOVER
    @AfterRecover()
    async afterRecoverUser(entity: User, context: HookContext<User>) {
        console.log(`User recovered successfully: ${entity.email} (ID: ${entity.id})`);

        // Restore related data
        // await this.sessionService.enableUserSessions(entity.id);
        // await this.tokenService.reissueUserTokens(entity.id);

        // Send recovery notification
        // await this.notificationService.notifyUserRecovery(entity);

        // Log recovery for audit
        // await this.auditService.logUserRecovery(entity, context.request?.user);

        return entity;
    }
}
Execution Order and Parameters

Hooks during Create process:

@BeforeCreate()  // = @BeforeAssign('create')
async beforeCreate(body: any, context: HookContext) {
  // body: request data
  // context: { operation: 'create', params: {}, currentEntity: undefined }
  return body; // Return modified body
}

@AfterCreate()   // = @AfterSave('create')
async afterCreate(entity: User, context: HookContext) {
  // entity: saved entity
  // context: { operation: 'create', params: {}, currentEntity: undefined }
  return entity; // Return modified entity
}

Hooks during Update process:

@BeforeUpdate()  // = @BeforeAssign('update')
async beforeUpdate(entity: User, context: HookContext) {
  // 🚀 NEW: entity-based (full entity with ID)
  // entity: entity to update (includes all fields)
  // context: { operation: 'update', params: { id: 5 }, currentEntity: User }
  return entity;
}

@AfterUpdate()   // = @AfterSave('update')
async afterUpdate(entity: User, context: HookContext) {
  // entity: updated entity
  // context: { operation: 'update', params: { id: 5 }, currentEntity: User }
  return entity;
}

🆕 NEW! Hooks during Destroy process:

@BeforeDestroy() // = @BeforeDestroy('destroy')
async beforeDestroy(entity: User, context: HookContext) {
  // 🚀 Entity-based (full entity with ID)
  // entity: entity to delete (includes all fields)
  // context: { operation: 'destroy', params: { id: 5 }, currentEntity: User }

  // Perfect for permission checks and cleanup preparation
  if (entity.status === 'locked') {
    throw new Error('Cannot delete locked user');
  }

  return entity;
}

@AfterDestroy()  // = @AfterDestroy('destroy')
async afterDestroy(entity: User, context: HookContext) {
  // entity: deleted entity (for cleanup and logging)
  // context: { operation: 'destroy', params: { id: 5 }, currentEntity: User }

  // Perfect for cleanup, notifications, and audit logs
  await this.cleanupUserData(entity.id);

  return entity;
}

🆕 NEW! Hooks during Recover process:

@BeforeRecover() // = @BeforeRecover('recover')
async beforeRecover(entity: User, context: HookContext) {
  // 🚀 Entity-based (full soft-deleted entity with ID)
  // entity: entity to recover (includes all fields)
  // context: { operation: 'recover', params: { id: 5 }, currentEntity: User }

  // Perfect for permission checks and recovery preparation
  if (entity.status === 'banned') {
    throw new Error('Cannot recover banned user');
  }

  // Set recovery metadata
  entity.recoveredBy = context.request?.user?.id;
  entity.recoveredAt = new Date();

  return entity;
}

@AfterRecover()  // = @AfterRecover('recover')
async afterRecover(entity: User, context: HookContext) {
  // entity: recovered entity (for restoration and logging)
  // context: { operation: 'recover', params: { id: 5 }, currentEntity: User }

  // Perfect for data restoration, notifications, and audit logs
  await this.restoreUserData(entity.id);

  return entity;
}

@BeforeShow() // = @BeforeAssign('show')
async beforeShow(params: any, context: HookContext) {
  // params: request parameters for entity lookup
  // context: { operation: 'show', params: { id: 5 } }
  
  // Transform parameters before entity lookup
  if (typeof params.id === 'string') {
    params.id = parseInt(params.id, 10);
  }
  
  return params;
}

@AfterShow()  // = @AfterAssign('show')
async afterShow(entity: User, context: HookContext) {
  // entity: retrieved entity
  // context: { operation: 'show', params: { id: 5 } }
  
  // Perfect for masking sensitive data, adding computed fields
  if (entity.password) {
    entity.password = '***HIDDEN***';
  }
  
  // Add view tracking
  await this.trackView(entity.id);
  
  return entity;
}
Advanced Usage Examples
@Crud({
    entity: Post,
    allowedParams: ['title', 'content', 'status'],
})
@Controller('posts')
export class PostController {
    constructor(public readonly crudService: PostService) {}

    @BeforeCreate()
    async beforeCreatePost(body: any, context: any) {
        // Auto-set user ID
        const userId = context.request?.user?.id;
        if (userId) {
            body.userId = userId;
        }

        // Auto-generate slug
        if (body.title && !body.slug) {
            body.slug = body.title
                .toLowerCase()
                .replace(/[^a-z0-9]/g, '-')
                .replace(/-+/g, '-')
                .replace(/^-|-$/g, '');
        }

        return body;
    }

    @BeforeSave('create')
    async validateBeforeSave(entity: Post, context: any) {
        // Check and resolve slug duplication
        const existingPost = await this.crudService.findBySlug(entity.slug);
        if (existingPost) {
            entity.slug = `${entity.slug}-${Date.now()}`;
        }

        return entity;
    }

    @AfterCreate()
    async afterCreatePost(entity: Post, context: any) {
        // Update search index
        // await this.searchService.indexPost(entity);

        // Notify about published post
        if (entity.status === 'published') {
            // await this.notificationService.notifyNewPost(entity);
            console.log(`New post published: ${entity.title}`);
        }

        return entity;
    }

    // Multiple hooks can be used together
    @BeforeUpdate()
    @BeforeUpsert()
    async beforeModify(data: any, context: any) {
        // Common logic for both UPDATE and UPSERT
        data.updatedAt = new Date();

        if (context.operation === 'upsert' && !context.currentEntity) {
            data.createdAt = new Date();
        }

        return data;
    }

    // 🆕 NEW! DESTROY hook example
    @BeforeDestroy()
    async beforeDelete(entity: Post, context: any) {
        console.log(`Preparing to delete post: ${entity.title} (ID: ${entity.id})`);

        // Check if user has permission to delete this post
        const userId = context.request?.user?.id;
        if (entity.userId !== userId) {
            const userRole = context.request?.user?.role;
            if (userRole !== 'admin' && userRole !== 'moderator') {
                throw new Error('Permission denied: Cannot delete other users posts');
            }
        }

        // Prevent deletion of published posts by regular users
        if (entity.status === 'published' && context.request?.user?.role !== 'admin') {
            throw new Error('Cannot delete published posts');
        }

        return entity;
    }

    @AfterDestroy()
    async afterDelete(entity: Post, context: any) {
        console.log(`Post deleted successfully: ${entity.title} (ID: ${entity.id})`);

        // Clean up related data
        // await this.commentService.deletePostComments(entity.id);
        // await this.tagService.removePostTags(entity.id);

        // Update search index
        // await this.searchService.removeFromIndex(entity.id);

        // Send notification
        // await this.notificationService.notifyPostDeletion(entity);

        return entity;
    }
}

🆕 Using new 4-stage detailed decorators:

import { Controller } from '@nestjs/common';
import {
    Crud,
    BeforeAssignCreate, // Before assignment
    AfterAssignCreate, // After assignment
    BeforeSaveCreate, // Before saving
    AfterSaveCreate, // After saving
    BeforeAssignUpdate,
    AfterSaveUpdate,
} from 'nestjs-crud';
import { User } from './user.entity';
import { UserService } from './user.service';
import * as bcrypt from 'bcrypt';

@Crud({
    entity: User,
    allowedParams: ['name', 'email', 'password', 'phone'],
})
@Controller('users')
export class UserController {
    constructor(public readonly crudService: UserService) {}

    // 🔐 CREATE: Stage 1 - Before assignment (data validation and transformation)
    @BeforeAssignCreate()
    async validateAndTransformCreate(body: any, context: any) {
        console.log('1️⃣ CREATE before assignment: Data validation and transformation');

        if (body.password) {
            body.password = await bcrypt.hash(body.password, 10);
        }

        // Set default values
        body.provider = body.provider || 'local';
        body.role = body.role || 'user';

        return body;
    }

    // 🔧 CREATE: Stage 2 - After assignment (entity post-processing)
    @AfterAssignCreate()
    async postProcessCreate(entity: User, context: any) {
        console.log('2️⃣ CREATE after assignment: Entity post-processing');

        // Additional entity processing logic
        if (!entity.displayName) {
            entity.displayName = entity.name;
        }

        return entity;
    }

    // 🔍 CREATE: Stage 3 - Before saving (final validation)
    @BeforeSaveCreate()
    async finalValidateCreate(entity: User, context: any) {
        console.log('3️⃣ CREATE before saving: Final validation');

        // Duplicate email check
        const existing = await this.crudService.findOne({ where: { email: entity.email } });
        if (existing) {
            throw new Error('Email already exists');
        }

        return entity;
    }

    // 📧 CREATE: Stage 4 - After saving (follow-up processing)
    @AfterSaveCreate()
    async postSaveCreate(entity: User, context: any) {
        console.log('4️⃣ CREATE after saving: Follow-up processing');

        // Send welcome email
        // await this.emailService.sendWelcomeEmail(entity);

        // Send analytics event
        // await this.analyticsService.trackUserCreated(entity);

        return entity;
    }

    // 🔐 UPDATE: Before assignment
    @BeforeAssignUpdate()
    async beforeUpdateAssign(entity: User, context: any) {
        console.log('🔄 UPDATE before assignment: Process update entity');

        // 🚀 NEW: Now receives full entity with ID
        if (entity.password) {
            entity.password = await bcrypt.hash(entity.password, 10);
        }

        entity.updatedAt = new Date();
        return entity;
    }

    // 📝 UPDATE: After saving
    @AfterSaveUpdate()
    async afterUpdateSave(entity: User, context: any) {
        console.log('📝 UPDATE after saving: Handle update completion');

        // Record update log
        // await this.auditService.logUserUpdate(entity, context.request?.user);

        return entity;
    }
}

Using consistent fine-grained control decorators:

@Controller('posts')
export class PostController {
    constructor(public readonly crudService: PostService) {}

    // Execute before assignment stage for all methods (create, update, upsert)
    @BeforeAssign('create')
    @BeforeAssign('update')
    @BeforeAssign('upsert')
    async commonPreProcess(body: any, context: any) {
        console.log(`🔧 ${context.operation.toUpperCase()} before assignment common processing`);

        // Common pre-processing logic
        body.updatedAt = new Date();
        if (context.operation === 'create') {
            body.createdAt = new Date();
        }

        return body;
    }

    // Execute after saving stage for all methods
    @AfterSave('create')
    @AfterSave('update')
    @AfterSave('upsert')
    async commonPostProcess(entity: any, context: any) {
        console.log(`✅ ${context.operation.toUpperCase()} after saving common processing`);

        // Common post-processing logic (search index update, cache refresh, etc.)
        // await this.searchService.updateIndex(entity);
        // await this.cacheService.invalidate(`post:${entity.id}`);

        return entity;
    }

    // 🆕 NEW! DESTROY-specific hooks
    @BeforeDestroy()
    async beforePostDelete(entity: any, context: any) {
        console.log(`🗑️ DESTROY before deletion: Preparing to delete ${entity.title}`);

        // Check deletion permissions
        const userId = context.request?.user?.id;
        if (entity.userId !== userId && context.request?.user?.role !== 'admin') {
            throw new Error('Permission denied');
        }

        // Set deletion metadata
        entity.deletedBy = userId;
        entity.deletedAt = new Date();

        return entity;
    }

    @AfterDestroy()
    async afterPostDelete(entity: any, context: any) {
        console.log(`🧹 DESTROY after deletion: Cleanup for ${entity.title}`);

        // Common cleanup logic
        // await this.searchService.removeFromIndex(entity.id);
        // await this.cacheService.invalidate(`post:${entity.id}`);
        // await this.notificationService.notifyDeletion(entity);

        return entity;
    }
}
🔗 Chain Execution of Multiple Hooks

When using the same decorator on multiple methods, they execute in definition order as a chain:

@Crud({
    entity: User,
    allowedParams: ['name', 'email', 'password'],
})
@Controller('users')
export class UserController {
    constructor(public readonly crudService: UserService) {}

    // 🔗 First CREATE hook
    @BeforeCreate()
    async validateData(body: any, context: any) {
        console.log('1️⃣ Validating data...');

        if (!body.email) {
            throw new Error('Email is required');
        }

        body.step1 = 'validated';
        return body; // ✅ Modified body passed to next hook
    }

    // 🔗 Second CREATE hook (receives result from first hook)
    @BeforeCreate()
    async hashPassword(body: any, context: any) {
        console.log('2️⃣ Hashing password...');
        console.log('Previous step result:', body.step1); // ✅ Outputs 'validated'

        if (body.password) {
            body.password = await bcrypt.hash(body.password, 10);
        }

        body.step2 = 'encrypted';
        return body; // ✅ Return final modified body
    }

    // 🔗 Third CREATE hook (receives result from second hook)
    @BeforeCreate()
    async setDefaults(body: any, context: any) {
        console.log('3️⃣ Setting defaults...');
        console.log('Previous steps results:', body.step1, body.step2); // ✅ Outputs 'validated', 'encrypted'

        body.provider = body.provider || 'local';
        body.role = body.role || 'user';
        body.step3 = 'completed';

        return body; // ✅ Final completed body
    }
}

Execution order:

POST /users
{
  "name": "John Doe",
  "email": "[email protected]",
  "password": "mypassword"
}

# Console output:
# 1️⃣ Validating data...
# 2️⃣ Hashing password...
# Previous step result: validated
# 3️⃣ Setting defaults...
# Previous steps results: validated encrypted

# Final saved data:
{
  "name": "John Doe",
  "email": "[email protected]",
  "password": "$2b$10$...", // ✅ Encrypted
  "provider": "local",      // ✅ Default set
  "role": "user",          // ✅ Default set
  "step1": "validated",    // ✅ Passed through chain
  "step2": "encrypted",    // ✅ Passed through chain
  "step3": "completed"     // ✅ Final processing
}
⚡ Simple Test Example
// Minimal example for simple testing
@Crud({
    entity: User,
    allowedParams: ['name', 'email', 'password'],
})
@Controller('users')
export class UserController {
    constructor(public readonly crudService: UserService) {}

    @BeforeCreate()
    async step1(body: any, context: any) {
        body.step1 = 'first';
        console.log('Step 1:', body);
        return body;
    }

    @BeforeCreate()
    async step2(body: any, context: any) {
        body.step2 = 'second';
        console.log('Step 2:', body); // Check if step1 exists
        return body;
    }
}

// POST /users { "name": "test" }
// Console output:
// Step 1: { name: "test", step1: "first" }
// Step 2: { name: "test", step1: "first", step2: "second" }
Advantages
  1. 🎯 Intuitive: Clear role from method names
  2. 🧹 Clean Code: Routes configuration not complex
  3. 🔗 Chain Execution: Multiple hooks execute sequentially with automatic data passing
  4. 🔄 Reusability: Common hooks can be implemented through inheritance
  5. 🛡️ Type Safety: TypeScript type checking support
  6. ✨ IntelliSense: IDE auto-completion support
  7. 🚀 Entity-based UPDATE/DESTROY/RECOVER: Full entity access with ID for advanced operations

🛠️ Method 2: Routes Configuration Approach (Legacy)

Basic Usage

@Controller('users')
@Crud({
    entity: User,
    routes: {
        create: {
            hooks: {
                assignBefore: async (body, context) => {
                    // Convert email to lowercase
                    if (body.email) {
                        body.email = body.email.toLowerCase();
                    }
                    return body;
                },

                assignAfter: async (entity, body, context) => {
                    // Set default role
                    if (!entity.role) {
                        entity.role = 'user';
                    }
                    return entity;
                },

                saveBefore: async (entity, context) => {
                    // Check duplicate email
                    const existing = await userService.findByEmail(entity.email);
                    if (existing) {
                        throw new Error('Email already exists');
                    }
                    return entity;
                },

                saveAfter: async (entity, context) => {
                    // Send welcome email
                    await emailService.sendWelcomeEmail(entity.email);
                    return entity;
                },
            },
        },

        update: {
            hooks: {
                assignBefore: async (entity, context) => {
                    // 🚀 NEW: Now receives full entity
                    // Auto-set update time
                    entity.updatedAt = new Date();

                    // Prevent modification of certain fields
                    const originalId = entity.id;
                    const originalCreatedAt = entity.createdAt;

                    // Restore protected fields if they were modified
                    entity.id = originalId;
                    entity.createdAt = originalCreatedAt;

                    return entity;
                },

                saveBefore: async (entity, context) => {
                    // Check permissions
                    const userId = context.request?.user?.id;
                    if (entity.id !== userId) {
                        throw new Error('Permission denied');
                    }
                    return entity;
                },
            },
        },

        destroy: {
            hooks: {
                destroyBefore: async (entity, context) => {
                    // 🆕 NEW! DESTROY before hook
                    console.log(`Preparing to delete user: ${entity.name} (ID: ${entity.id})`);

                    // Check delete permission
                    const userId = context.request?.user?.id;
                    if (entity.id !== userId && context.request?.user?.role !== 'admin') {
                        throw new Error('Permission denied: Cannot delete other users');
                    }

                    // Prevent deletion of locked accounts
                    if (entity.status === 'locked') {
                        throw new Error('Cannot delete locked user account');
                    }

                    // Set deletion metadata
                    entity.deletedBy = userId;
                    entity.deletedAt = new Date();

                    return entity;
                },

                destroyAfter: async (entity, context) => {
                    // 🆕 NEW! DESTROY after hook
                    console.log(`User deleted successfully: ${entity.name} (ID: ${entity.id})`);

                    // Cleanup related data
                    // await sessionService.revokeUserSessions(entity.id);
                    // await tokenService.revokeUserTokens(entity.id);

                    // Send deletion notification
                    // await emailService.notifyUserDeletion(entity);

                    // Log for audit
                    // await auditService.logUserDeletion(entity, context.request?.user);

                    return entity;
                },
            },
        },

        recover: {
            hooks: {
                recoverBefore: async (entity, context) => {
                    // 🆕 NEW! RECOVER before hook
                    console.log(`Preparing to recover user: ${entity.name} (ID: ${entity.id})`);

                    // Check recover permission
                    const userId = context.request?.user?.id;
                    if (entity.deletedBy !== userId && context.request?.user?.role !== 'admin') {
                        throw new Error('Permission denied: Cannot recover other users data');
                    }

                    // Prevent recovery of banned accounts
                    if (entity.status === 'banned') {
                        throw new Error('Cannot recover banned user account');
                    }

                    // Set recovery metadata
                    entity.recoveredBy = userId;
                    entity.recoveredAt = new Date();

                    return entity;
                },

                recoverAfter: async (entity, context) => {
                    // 🆕 NEW! RECOVER after hook
                    console.log(`User recovered successfully: ${entity.name} (ID: ${entity.id})`);

                    // Restore related data
                    // await sessionService.enableUserSessions(entity.id);
                    // await tokenService.reissueUserTokens(entity.id);

                    // Send recovery notification
                    // await emailService.notifyUserRecovery(entity);

                    // Log for audit
                    // await auditService.logUserRecovery(entity, context.request?.user);

                    return entity;
                },
            },
        },
    },
})
export class UserController {
    constructor(public readonly crudService: UserService) {}
}

Advanced Usage Example

@Controller('posts')
@Crud({
    entity: Post,
    routes: {
        create: {
            hooks: {
                assignBefore: async (body, context) => {
                    // Auto-set user ID
                    const userId = context.request?.user?.id;
                    if (userId) {
                        body.userId = userId;
                    }

                    // Auto-generate slug
                    if (body.title && !body.slug) {
                        body.slug = slugify(body.title);
                    }

                    return body;
                },

                assignAfter: async (entity, body, context) => {
                    // Set default post status
                    if (!entity.status) {
                        entity.status = 'draft';
                    }

                    // Set publication date when publishing
                    if (entity.status === 'published' && !entity.publishedAt) {
                        entity.publishedAt = new Date();
                    }

                    return entity;
                },

                saveBefore: async (entity, context) => {
                    // Validate required fields
                    if (!entity.title?.trim()) {
                        throw new Error('Title is required');
                    }

                    // Check and resolve slug duplication
                    const existingPost = await postService.findBySlug(entity.slug);
                    if (existingPost) {
                        entity.slug = `${entity.slug}-${Date.now()}`;
                    }

                    return entity;
                },

                saveAfter: async (entity, context) => {
                    // Update search index
                    await searchService.indexPost(entity);

                    // Process tags
                    if (entity.tags?.length) {
                        await tagService.processPostTags(entity.id, entity.tags);
                    }

                    // Notify about published post
                    if (entity.status === 'published') {
                        await notificationService.notifyNewPost(entity);
                    }

                    return entity;
                },
            },
        },

        upsert: {
            hooks: {
                assignBefore: async (body, context) => {
                    const now = new Date();
                    body.updatedAt = now;

                    // Set creation date only for new data
                    if (!context.currentEntity) {
                        body.createdAt = now;
                    }

                    return body;
                },

                saveAfter: async (entity, context) => {
                    // Differentiate between newly created and updated cases
                    const isNew = !context.currentEntity;

                    if (isNew) {
                        await analyticsService.trackPostCreated(entity);
                    } else {
                        await analyticsService.trackPostUpdated(entity);
                    }

                    return entity;
                },
            },
        },
    },
})
export class PostController {
    constructor(public readonly crudService: PostService) {}
}

HookContext Usage

// HookContext provides the following information
interface HookContext<T> {
    operation: 'create' | 'update' | 'upsert' | 'destroy' | 'recover' | 'show'; // Operation type
    params?: Record<string, any>; // URL parameters
    currentEntity?: T; // Current entity (update, upsert, destroy, recover)
    request?: any; // Express Request object
}

// Context usage example
const hooks = {
    assignBefore: async (body, context) => {
        console.log(`Operation type: ${context.operation}`);

        // Use requester information
        if (context.request?.user) {
            body.lastModifiedBy = context.request.user.id;
        }

        // Use URL parameters
        if (context.params?.parentId) {
            body.parentId = context.params.parentId;
        }

        // Use existing entity information (update, upsert only)
        if (context.currentEntity) {
            console.log('Existing data:', context.currentEntity);
        }

        return body;
    },
};

Show Operation Hooks (NEW! 🆕)

The show operation now supports assignBefore and assignAfter hooks for read-time data processing:

  • assignBefore: Process query parameters before entity lookup (security filtering, parameter transformation)
  • assignAfter: Process retrieved entity before returning it (data masking, calculated fields)
Show Operation Hook Examples
@Controller('users')
@Crud({
    entity: User,
    routes: {
        show: {
            hooks: {
                // Process parameters before entity lookup
                assignBefore: async (params, context) => {
                    // Security: Validate user has permission to view this ID
                    const requesterId = context.request?.user?.id;
                    const targetId = params.id;
                    
                    if (requesterId !== targetId && context.request?.user?.role !== 'admin') {
                        throw new ForbiddenException('Cannot view other users');
                    }
                    
                    // Parameter transformation
                    if (typeof params.id === 'string') {
                        params.id = parseInt(params.id, 10);
                    }
                    
                    return params;
                },
                
                // Process entity after retrieval
                assignAfter: async (entity, _, context) => {
                    // Mask sensitive information
                    if (entity.email) {
                        entity.email = entity.email.replace(/(.{2}).*(@.*)/, '$1***$2');
                    }
                    
                    // Hide sensitive fields based on viewer's role
                    if (context.request?.user?.role !== 'admin') {
                        delete entity.salary;
                        delete entity.ssn;
                    }
                    
                    // Add calculated fields
                    (entity as any).displayName = `${entity.firstName} ${entity.lastName}`;
                    (entity as any).age = calculateAge(entity.birthDate);
                    
                    // Track view statistics (without saving to DB)
                    await analyticsService.trackView(entity.id, context.request?.user?.id);
                    
                    return entity;
                }
            }
        }
    }
})
export class UserController {
    constructor(public readonly crudService: UserService) {}
}
Common Use Cases for Show Hooks
  1. Data Masking & Security:
assignAfter: async (entity, _, context) => {
    // Mask PII based on viewer permissions
    const isOwner = entity.id === context.request?.user?.id;
    const isAdmin = context.request?.user?.role === 'admin';
    
    if (!isOwner && !isAdmin) {
        entity.phone = entity.phone?.replace(/\d(?=\d{4})/g, '*');
        entity.email = entity.email?.replace(/(.{2}).*(@.*)/, '$1***$2');
        delete entity.dateOfBirth;
    }
    
    return entity;
}
  1. Enriching Response with Calculated Data:
assignAfter: async (entity, _, context) => {
    // Add computed fields without modifying database
    (entity as any).fullAddress = `${entity.street}, ${entity.city}, ${entity.country}`;
    (entity as any).accountAge = daysSince(entity.createdAt);
    (entity as any).isPremium = entity.subscriptionLevel === 'premium';
    
    // Fetch additional data from external services
    (entity as any).creditScore = await creditService.getScore(entity.id);
    
    return entity;
}
  1. Parameter Validation & Transformation:
assignBefore: async (params, context) => {
    // Validate and transform ID formats
    if (params.uuid && !isValidUUID(params.uuid)) {
        throw new BadRequestException('Invalid UUID format');
    }
    
    // Apply tenant isolation
    if (context.request?.tenant) {
        params.tenantId = context.request.tenant.id;
    }
    
    return params;
}
  1. Analytics & Monitoring:
assignAfter: async (entity, _, context) => {
    // Track access patterns without affecting response
    await Promise.all([
        auditLog.record({
            action: 'VIEW',
            entityType: 'User',
            entityId: entity.id,
            viewerId: context.request?.user?.id,
            timestamp: new Date()
        }),
        metricsService.increment('user.profile.views'),
        cacheService.warm(entity.id, entity) // Pre-cache for future requests
    ]);
    
    return entity;
}

Reusing Common Hook Functions

// Define