Crudify is a configurable CRUD generator for NestJS and Mongoose. It creates a complete set of REST endpoints for a Mongoose model, generates Swagger documentation, supports runtime route enable/disable, soft deletes, bulk operations, advanced query parsing and optional MongoDB-backed audit logging.
You write the model, service and controller. Crudify generates the routes and the repetitive CRUD logic.
- Requirements
- Installation
- Quick start
- Generated endpoints
- Core concepts
- Runtime configuration
- Audit logging
- Query language
- Soft delete
- Overriding generated behavior
- Swagger
- Error handling
- Health check
- Documentation
- Troubleshooting
- Contributing
- License
| Requirement | Version |
|---|---|
| Node.js | >=20.19.0 |
| NestJS | 12.x |
@nestjs/mongoose |
12.x |
@nestjs/swagger |
12.x |
| Mongoose | 9.x |
| TypeScript | >=5.x |
npm install ncrudify @nestjs/mongoose mongoose @nestjs/swaggerDepending on your NestJS and Express setup, you may also need the Swagger UI package:
npm install swagger-ui-express// app.module.ts
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { CrudifyModule } from "ncrudify";
import { UserModule } from "./user/user.module";
@Module({
imports: [
MongooseModule.forRoot(process.env.MONGODB_URI!),
CrudifyModule.forRoot(),
UserModule,
],
})
export class AppModule {}CrudifyModule.forRoot() registers the runtime configuration module, the health endpoint and the optional audit module.
Crudify uses bracket-style query parameters such as age[gte]=18. With Express, enable the extended query parser in main.ts:
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.set("query parser", "extended");
await app.listen(3000);
}
bootstrap();Without this setting, nested filters and arrays may not be parsed correctly.
// user/entities/user.entity.ts
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { ApiProperty } from "@nestjs/swagger";
import { HydratedDocument } from "mongoose";
export type UserDocument = HydratedDocument<User>;
@Schema({ timestamps: true })
export class User {
@ApiProperty({ example: "John Doe" })
@Prop({ required: true })
name!: string;
@ApiProperty({ example: "john@example.com" })
@Prop({ required: true, unique: true })
email!: string;
@ApiProperty({ example: 30, required: false })
@Prop()
age?: number;
}
export const UserSchema = SchemaFactory.createForClass(User);// user/user.service.ts
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { CrudifyService } from "ncrudify";
import { User } from "./entities/user.entity";
@Injectable()
export class UserService extends CrudifyService<User> {
constructor(@InjectModel(User.name) protected readonly userModel: Model<User>) {
super(userModel);
}
}// user/user.controller.ts
import { Controller } from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import { Crudify, CrudifyController } from "ncrudify";
import { User, UserSchema } from "./entities/user.entity";
import { UserService } from "./user.service";
@ApiTags("users")
@Crudify({
model: {
type: User,
schema: UserSchema,
},
})
@Controller("users")
export class UserController extends CrudifyController<User> {
constructor(public readonly service: UserService) {
super(service);
}
}// user/user.module.ts
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { User, UserSchema } from "./entities/user.entity";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}You now have a complete CRUD API under /users.
| Method | Path | Description |
|---|---|---|
POST |
/users |
Create one record |
POST |
/users/bulk |
Create many records |
GET |
/users |
Find records with filters, sorting and paging |
GET |
/users/count |
Count records matching filters |
GET |
/users/:id |
Find one record by ID |
PATCH |
/users/:id |
Partially update one record |
PATCH |
/users/bulk |
Update many records by filter |
PUT |
/users/:id |
Replace one record |
DELETE |
/users/:id |
Delete one record (or soft delete when enabled) |
DELETE |
/users/:id/soft |
Soft delete one record |
DELETE |
/users/:id/restore |
Restore one soft-deleted record |
DELETE |
/users/bulk |
Delete many records by filter |
PATCH |
/users/bulk/restore |
Restore many soft-deleted records by filter |
See docs/routes.md for request bodies, responses and curl examples.
@Crudify({
model: {
type: User,
cdto: CreateUserDto,
udto: UpdateUserDto,
schema: UserSchema,
},
softDelete: true,
routes: {
exclude: ["deleteBulk"],
disableBulk: false,
decorators: [UseGuards(AuthGuard)],
config: {
findAll: {
decorators: [ApiBearerAuth()],
},
delete: {
disabled: true,
},
},
},
})
@Controller("users")
export class UserController extends CrudifyController<User> {}| Option | Description |
|---|---|
model.type |
Mongoose model class. Required. |
model.cdto |
DTO used for create requests. Defaults to the model class. |
model.udto |
DTO used for update/replace requests. Defaults to the model class. |
model.schema |
Mongoose schema used for Swagger filter generation. |
softDelete |
Makes DELETE /:id update deletedAt instead of removing the document. |
routes.exclude |
Routes that must not be publicly available. They are hidden from Swagger. |
routes.disableBulk |
Disables all bulk routes. |
routes.decorators |
Decorators applied to every enabled route. |
routes.config |
Per-route decorators and disabled flag. |
The complete reference is in docs/crudify-decorator.md.
Extend CrudifyService<T> to inherit all CRUD operations. Every method can be overridden:
async findAll(query: any) {
const result = await super.findAll(query);
return {
...result,
results: result.results.filter((user) => user.active),
};
}The complete service API is in docs/api-reference.md.
Extend CrudifyController<T> to inherit the generated route handlers. You can override any handler while keeping the generated route decorators.
@Get(":id")
findOne(@Param("id") id: string) {
return this.service.findOne({ _id: id });
}Crudify can enable or disable a model or individual CRUD routes at runtime, without redeploying the application. Configuration is stored in MongoDB and cached in memory.
CrudifyModule.forRoot();Available endpoints:
| Method | Path | Description |
|---|---|---|
GET |
/config |
Return the current configuration cache |
PUT |
/config/:model |
Update or create the configuration for a model |
POST |
/config/reload |
Reload the configuration cache from MongoDB |
Example:
curl -X PUT http://localhost:3000/config/User \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"routes": {
"delete": false,
"deleteBulk": false
},
"props": {
"group": "admin"
}
}'Setting enabled: false disables every route for that model. Per-route flags are evaluated only when the model is enabled. See docs/config.md.
Audit logging is opt-in. It records HTTP requests in MongoDB and supports exclusions with @NoAudit().
CrudifyModule.forRoot({
audit: {
enabled: true,
collectionName: "audit_logs",
userExtractor: (request) =>
request.user?._id ?? request.user?.email ?? "anonymous",
},
});Every request stores:
interface AuditLogRecord {
user: string;
action: string;
entityId: string | null;
details: Record<string, any>;
createdAt?: Date | string;
}The details object contains method, url, payload, oldDocument (for PATCH, PUT and DELETE when the model can be resolved), statusCode, durationMs and, for failed requests, error.
Exclude routes:
import { NoAudit } from "ncrudify";
@NoAudit("GET/:id")
@Get(":id")
findOne() {
// ...
}Audit also exposes:
GET /auditâ read audit logs with filters, sorting and pagination.GET /audit/:idâ read one audit log.AuditServiceâ injectable service for manual logging.
See docs/audit.md for the complete reference.
GET /users supports filtering, sorting, pagination and population through query parameters:
GET /users?age[gte]=18&status[in]=active,pending&sort=-createdAt&limit=10&skip=20
Supported operators include eq, ne, gt, gte, lt, lte, starts, ends, cont, excl, in, notin, isnull, notnull and between.
The complete query language is documented in docs/query-parser.md.
Enable soft delete per controller:
@Crudify({
model: {
type: User,
},
softDelete: true,
})
@Controller("users")
export class UserController extends CrudifyController<User> {}With soft delete enabled:
DELETE /users/:idsetsdeletedAtinstead of removing the document.DELETE /users/:id/softalways performs a soft delete.DELETE /users/:id/restoreremovesdeletedAt.PATCH /users/bulk/restorerestores many documents.findAllandfindOnedo not automatically exclude soft-deleted documents. AdddeletedAtfilters or override the service if you need automatic filtering.
You can override service methods, controller methods, or both.
@Injectable()
export class UserService extends CrudifyService<User> {
constructor(@InjectModel(User.name) model: Model<User>) {
super(model);
}
async findAll(query: any) {
const result = await super.findAll(query);
return {
...result,
results: result.results.sort((a, b) => a.name.localeCompare(b.name)),
};
}
}@Crudify({ model: { type: User } })
@Controller("users")
export class UserController extends CrudifyController<User> {
constructor(public service: UserService) {
super(service);
}
@Get(":id")
findOne(@Param("id") id: string) {
return this.service.findOne({ _id: id });
}
}Crudify adds Swagger metadata to every generated route, including operation IDs, request bodies, response types and filter examples.
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
const config = new DocumentBuilder()
.setTitle("Crudify API")
.setDescription("API generated with Crudify")
.setVersion("1.0")
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("docs", app, document, {
swaggerOptions: { filter: true },
});See docs/swagger.md for tags, authentication, route-specific decorators and disabled-route behavior.
CrudifyController uses CrudifyExceptionFilter, which maps common errors to consistent HTTP responses:
| Error | Status | Response error |
|---|---|---|
Mongoose CastError |
400 |
Bad Request |
Mongoose ValidationError |
400 |
Bad Request |
Duplicate key (code: 11000) |
409 |
Conflict |
HttpException |
own | own |
| Unknown error | 500 |
Internal Server Error |
Response shape:
{
"statusCode": 400,
"message": "Cast to ObjectId failed for value ...",
"error": "Bad Request"
}See docs/exceptions.md.
GET /health checks the application and MongoDB connection:
{
"status": "OK",
"database": "Connected",
"timestamp": "2026-10-22T10:00:00.000Z"
}When MongoDB is not connected, the endpoint returns 503 Service Unavailable with the same payload shape. See docs/health.md.
| Document | Contents |
|---|---|
| Getting started | Installation, module setup and first CRUD API |
| Crudify decorator | All @Crudify options and route customization |
| Routes | Every generated endpoint with bodies, responses and examples |
| Query parser | Filters, operators, sorting, pagination and population |
| Runtime configuration | Enable/disable models and routes at runtime |
| Audit logging | Audit records, exclusions, reading logs and manual logging |
| Swagger | Swagger setup, tags, auth and generated operations |
| Exceptions | Error mapping and response format |
| Health | Health endpoint and connection states |
| API reference | Exported classes, decorators, interfaces and method signatures |
| Migration to 1.5.0 | Upgrade notes for the audit release |
| Troubleshooting | Common problems and solutions |
- Filters do not work: enable
app.set("query parser", "extended"). @Crudifythrowsrequires options.model.type: pass the Mongoose model class inmodel.type.- Audit logs are not created: verify
audit.enabled: true, check that the request is not excluded by@NoAudit, and inspect the Nest logger for audit write errors. oldDocumentis missing: it is available only forPATCH,PUTandDELETErequests with anidparam on controllers decorated with@Crudify.- Soft-deleted records still appear:
findAlldoes not filterdeletedAtautomatically.
See docs/troubleshooting.md for more.
Contributions are welcome. Open an issue or submit a pull request on GitHub.
Before opening a pull request:
npm test
npm run buildUse Conventional Commits for commit messages. Releases are automated with semantic-release; see .github/RELEASE.md.
MIT. See LICENSE.