SuperAPI AI Directory

nestjs-crud-v1

Coding & development

View repository

Updated 10-16 06:22

About this skill

@nestjs-crud (v1.0.x — legacy)

Auto-generates RESTful CRUD endpoints from a single @Crud() decorator. Three adapters: TypeORM, Drizzle, MikroORM. Maintenance-line only — new projects should use v2.

Install

# Pick adapter
npm install @nestjs-crud/core @nestjs-crud/typeorm
npm install @nestjs-crud/core @nestjs-crud/drizzle
npm install @nestjs-crud/core @nestjs-crud/mikro-orm

# Optional frontend query builder
npm install @nestjs-crud/request

Required peer deps. v1 declares only some of them (undeclared ones marked ✗), so install all explicitly:

  • @nestjs-crud/core peers: class-validator, class-transformer
  • @nestjs-crud/typeorm ALSO needs: typeorm ✗, @nestjs/typeorm ✗, @nestjs/common ✗. It declares no peers.
  • @nestjs-crud/drizzle ALSO needs: drizzle-orm (≥ 0.36.0), @nestjs/common ✗
  • @nestjs-crud/mikro-orm ALSO needs: @mikro-orm/core (≥ 6.0.0), @mikro-orm/knex (≥ 6.0.0), @nestjs/common

npm install will NOT warn about ✗ peers. Symptoms at runtime: TypeError: repo.createQueryBuilder is not a function or Cannot find module 'typeorm'. Fixed in v2 (every adapter declares full peers).

Global Defaults — CrudConfigService.load()

Set project-wide defaults in main.ts BEFORE NestFactory.create. Every @Crud() deep-merges; controller scalars win, arrays replace.

// src/main.ts
import { CrudConfigService } from '@nestjs-crud/core';

CrudConfigService.load({
  query: { limit: 25, maxLimit: 100, alwaysPaginate: true, cache: 2000, softDelete: false },
  routes: { updateOneBase: { allowParamsOverride: false }, deleteOneBase: { returnDeleted: false } },
  params: { id: { field: 'id', type: 'number', primary: true } },
  serialize: { getMany: false },
  auth: { property: 'user' },
});

const app = await NestFactory.create(AppModule);

Default without load(): alwaysPaginate: false, no global params, no auth defaults.

Quickstart

Controller + @Crud() identical across adapters. Only service constructor differs.

@Crud({
  model: { type: User },
  query: {
    limit: 25,
    maxLimit: 100,
    join: { profile: { eager: true }, posts: { allow: ['id', 'title'] } },
  },
})
@Controller('users')
export class UsersController implements CrudController<User> {
  constructor(public service: UsersService) {}
}

TypeORM service

@Injectable()
export class UsersService extends TypeOrmCrudService<User> {
  constructor(@InjectRepository(User) repo: Repository<User>) {
    super(repo);
  }
}

Drizzle service

@Injectable()
export class UsersService extends DrizzleCrudService<typeof users.$inferSelect> {
  constructor(@Inject('DB') drizzleDb: typeof db) {
    super(drizzleDb, users); // (db instance, table reference)
  }
}

MikroORM service

@Injectable()
export class UsersService extends MikroOrmCrudService<User> {
  constructor(em: EntityManager) {
    super(em, User);
  }
}

MikroOrmCrudService accepts EntityManager directly. For @InjectRepository(User) style, unwrap manually:

constructor(@InjectRepository(User) repo: EntityRepository<User>) {
  super(repo.getEntityManager(), User);
}

(v2.2.0+ accepts EntityManager | EntityRepository<T> directly — no unwrap.)

Generated Endpoints

Method Path Handler
GET /users getManyBase
GET /users/:id getOneBase
POST /users createOneBase
POST /users/bulk createManyBase
PATCH /users/:id updateOneBase
PUT /users/:id replaceOneBase
DELETE /users/:id deleteOneBase
PATCH /users/:id/recover recoverOneBase

Query Params (Frontend → Backend)

import { RequestQueryBuilder, CondOperator } from '@nestjs-crud/request';

const qb = RequestQueryBuilder.create()
  .setFilter({ field: 'isActive', operator: CondOperator.EQUALS, value: true })
  .setOr({ field: 'role', operator: CondOperator.IN, value: ['admin', 'mod'] })
  .setJoin({ field: 'profile' })
  .sortBy({ field: 'createdAt', order: 'DESC' })
  .setLimit(20)
  .setPage(2);

fetch(`/users?${qb.query()}`);

23 operators (CondOperator enum):

  • Equality / comparison: $eq, $ne, $gt, $gte, $lt, $lte
  • Range / null: $between, $isnull, $notnull
  • Set: $in, $notin
  • String match: $cont, $excl, $starts, $ends
  • Case-insensitive variants: $eqL, $neL, $inL, $notinL, $contL, $exclL, $startsL, $endsL

Raw form: ?filter=name||$cont||john&sort=createdAt,DESC&join=profile&limit=25&page=2

@Crud() Key Options

@Crud({
  model: { type: Entity },                                // required

  dto: { create: CreateDto, update: UpdateDto, replace: ReplaceDto },
  serialize: { getMany: ListResponse, get: ItemResponse },

  query: {
    limit: 25, maxLimit: 100,
    cache: 2000,                                          // ms — TypeORM only; Drizzle/MikroORM no-op
    alwaysPaginate: false,
    softDelete: false,                                    // enables recoverOneBase
    join: { rel: { eager: true, allow: ['id', 'name'], exclude: ['secret'], required: false } },
    filter: [{ field: 'deletedAt', operator: '$isnull' }],
    sort: [{ field: 'id', order: 'ASC' }],
    exclude: ['password'],
  },

  routes: {
    exclude: ['createManyBase'],
    updateOneBase: { allowParamsOverride: false, returnShallow: false },
    deleteOneBase: { returnDeleted: false },
  },

  params: { id: { field: 'id', type: 'uuid', primary: true } },
  validation: { whitelist: true },
})

recoverOneBase requires query.softDelete: true + soft-delete column on entity.

Scoping Requests Per User (@CrudAuth)

@Crud({ model: { type: Post } })
@CrudAuth({
  property: 'user',
  filter: (user: User) => ({ authorId: { $eq: user.id } }),
  persist: (user: User) => ({ authorId: user.id }),
})
@UseGuards(JwtAuthGuard)
@Controller('posts')
export class PostsController implements CrudController<Post> {
  constructor(public service: PostsService) {}
}
  • filter — appended to every read
  • persist — set on every write (prevents spoofing)
  • property — where to find user on req (default 'user')

v1 footgun: persist typos against entity column names are SILENTLY dropped at runtime. persist: { user_id: ... } against entity column userId = auth-filter bypass on writes. Audit every @CrudAuth({ persist: {...} }) block by hand. v2 throws RequestQueryException on typo.

Guard ordering: @UseGuards() MUST come before @CrudAuth() reads req.user. Place auth guard on controller class.

Overriding Generated Routes (@Override)

@Override('getOneBase')
@UseInterceptors(MyInterceptor)
getUser(@ParsedRequest() req: CrudRequest, @Param('id') id: string) {
  return this.service.getOne(req);
}

@Override('createOneBase')
createUser(@ParsedRequest() req: CrudRequest, @ParsedBody() dto: User) {
  return this.service.createOne(req, dto);
}

Use @ParsedBody() not @Body() — @Body() bypasses class-validator group selection (CREATE and UPDATE collapse to identical validation).

@Override accepts: getManyBase, getOneBase, createOneBase, createManyBase, updateOneBase, replaceOneBase, deleteOneBase, recoverOneBase.

DTOs — Two Patterns

Pattern A: Entity-as-DTO (default). class-validator groups on entity:

import { CrudValidationGroups } from '@nestjs-crud/core';
const { CREATE, UPDATE } = CrudValidationGroups;

@Entity()
export class User {
  @IsOptional({ groups: [UPDATE] })
  @IsNotEmpty({ groups: [CREATE] })
  @IsString({ always: true })
  @Column()
  name: string;
}

CREATE runs on POST + PUT; UPDATE on PATCH; { always: true } on both.

Pattern B: Dedicated DTO classes via @Crud({ dto: { create, update, replace } }). Use when write shape differs meaningfully from entity (nested input, computed fields, OpenAPI hardening). With dto, CrudValidationGroups is NOT used — controller validates against DTO class directly.

Output serialization is separate. Combine entity-in (Pattern A) + DTO-out (@Crud({ serialize: {...} })) freely.

Swagger

Install @nestjs/swagger — auto-generates. Library safeRequire-skips when absent.

v2 heads-up: ParamOption.enum's SwaggerEnumType is internal-import-pathed in v1 (@nestjs/swagger/dist/types/swagger-enum.type); v2 inlines it (one-line swap if you import same path directly). v2 also rewrites default Swagger text + adds @Crud({ swagger: {...} }) override surface — snapshot-test OpenAPI? Expect drift. See nestjs-crud-migration skill §C–§D.

Best Practices

  • CondOperator enum, never raw strings. '$cont' silently misspells; CondOperator.CONTAINS is TypeScript-checked.
  • CrudValidationGroups constants, never magic strings. 'update' !== CrudValidationGroups.UPDATE.
  • Always exclude sensitive fields in query config. Without it, ?fields=password returns it.
  • Always allow-list join fields on sensitive relations. Without allow, joined relation returns all columns.
  • Use search for AND/OR composition; filter is AND-only. Raw: ?s={"$or":[{"name":{"$contL":"a"}},{"email":{"$contL":"a"}}]}
  • Set maxLimit server-side — never trust client limit alone. Client omitting limit returns all rows without maxLimit.

Common Issues

Symptom Cause + Fix
Relations not loading Add to query.join. Without it, client join requests silently ignored.
maxLimit exceeded → 400 Raise maxLimit or set alwaysPaginate: false.
Validation always fails on update Fields need @IsOptional({ groups: [UPDATE] }).
@CrudAuth filter not applying Auth guard must run BEFORE CrudRequestInterceptor. Place @UseGuards() on class.
Flat array instead of { data, count, total, page, pageCount } Set alwaysPaginate: true inside query: (NOT top-level) or via CrudConfigService.load().
TypeError: repo.createQueryBuilder is not a function / Cannot find module 'typeorm' Adapter peer not installed. v1 doesn't declare peers — install per §Install list manually.
Swagger metadata empty @nestjs/swagger not installed. safeRequire skips Swagger setup; install + restart.
@CrudAuth filter applies but persisted writes have wrong values (or missing) Typo in a persist key, silently dropped (see §@CrudAuth footgun). Fix the key.
MikroORM: stale entity across requests / em.flush() doesn't persist Subclass cached em at constructor time, breaking per-request identity-map isolation. Resolve em fresh inside every method. v2 enforces structurally via getEm thunk.
@Crud({ query: { cache: N } }) does nothing on Drizzle/MikroORM Only TypeORM honors cache in v1. Drizzle/MikroORM silently no-op. Use ORM's native cache primitive at app layer. v2: unified CacheStrategy interface across all 4 adapters.

Pinning v1.0.x

{
  "dependencies": {
    "@nestjs-crud/core": "^1.0.2",
    "@nestjs-crud/typeorm": "^1.0.2"
  }
}

npm update continues to track v1.0.x. Bugfix patches still land. New features land in v2 only — see nestjs-crud-migration skill for upgrade path.