@faster-crud: Killing the CRUD Boilerplate in NestJS
@faster-crud: Killing the CRUD Boilerplate in NestJS
GitHub: bkmashiro/nest-faster-crud
npm:@faster-crud/core,@faster-crud/nest,@faster-crud/typeorm,@faster-crud/vue
The Problem
Writing CRUD in NestJS is a ritual. For every new resource, you write roughly the same things in roughly the same order:
- A TypeORM entity
- A
CreateDtowith@IsString(),@IsOptional(), etc. - An
UpdateDto(usuallyPartialType(CreateDto)but with exceptions) - A service with
findAll,findOne,create,update,remove - A controller wiring those up with
@Get,@Post,@Patch,@Delete @ApiTags,@ApiOperation,@ApiResponsefor Swagger- A module that imports and exports everything
Then you do it again for the next resource. And the next. By the third resource you’re not thinking about what you’re building — you’re on autopilot, copy-pasting and find-replacing.
I’ve been in that loop enough times. So I built a way out.
The Solution: defineCrud<T>()
@faster-crud is a library that generates the full CRUD stack from a single factory call. The entry point is defineCrud<T>(opts):
import { defineCrud } from '@faster-crud/nest';
import { User } from './user.entity';
const UserCrud = defineCrud<User>({
entity: User,
createFields: ['name', 'email', 'role'],
updateFields: ['name', 'role'],
searchFields: ['name', 'email'],
});
@Module({
imports: [TypeOrmModule.forFeature([User]), UserCrud.module],
controllers: [...UserCrud.controllers],
providers: [...UserCrud.providers],
})
export class UserModule {}
That’s it. defineCrud returns a fully wired NestJS module with:
- Generated
CreateDtoandUpdateDto(derived from entity field types via reflection) - A service backed by the TypeORM adapter
- A controller with
GET /users,GET /users/:id,POST /users,PATCH /users/:id,DELETE /users/:id - Swagger decorators on every endpoint
- A
GET /__crud/metaintrospection endpoint (more on this below)
All fields are typed. You get TypeScript autocomplete on createFields, updateFields, and searchFields — they’re (keyof T)[]. Typos become compile errors.
Architecture: Three Layers
The library is split into three packages to keep concerns separate and enable future framework portability.
@faster-crud/core (v0.1.0)
Zero NestJS dependencies. Defines the CrudDefinition<T> interface, the field metadata model, and the ResourceMeta schema. This is the framework-agnostic heart of the system.
Because core has no framework deps, the same definition format can theoretically be adapted to Hono, Fastify, or any other Node.js framework. NestJS is just the first adapter.
@faster-crud/nest (v0.1.1)
The NestJS integration layer. Takes a CrudDefinition<T> and uses NestJS’s DynamicModule API to produce a ready-to-import module. Handles DTO class generation via reflect-metadata, controller class construction, and Swagger integration.
One non-obvious detail: NestJS requires DTO classes (not just interfaces) for validation pipe integration and Swagger schema generation. @faster-crud/nest generates these classes dynamically at runtime using a class factory, then decorates them with class-validator and @nestjs/swagger metadata before handing them to NestJS’s IoC container.
@faster-crud/typeorm (v0.1.0)
The TypeORM adapter. Provides the service implementation backed by a TypeORM repository. Implements the CrudService<T> interface from core.
// Internally, the TypeORM adapter looks roughly like this:
class TypeOrmCrudService<T> implements CrudService<T> {
constructor(private readonly repo: Repository<T>) {}
findAll(query: SearchQuery<T>): Promise<T[]> {
return this.repo.find({ where: buildWhere(query) });
}
// ... create, update, remove
}
Swapping out TypeORM for Prisma or another ORM would mean writing a new adapter that satisfies CrudService<T> — the rest of the stack stays unchanged.
The /__crud/meta Introspection Endpoint
Every defineCrud call also registers a GET /__crud/meta endpoint that returns the full ResourceMeta JSON for that resource:
{
"resource": "User",
"fields": [
{ "name": "name", "type": "string", "required": true, "label": "Name" },
{ "name": "email", "type": "string", "required": true, "label": "Email" },
{ "name": "role", "type": "string", "required": false, "label": "Role" }
],
"endpoints": {
"list": "GET /users",
"get": "GET /users/:id",
"create": "POST /users",
"update": "PATCH /users/:id",
"delete": "DELETE /users/:id"
}
}
This meta endpoint is the bridge to the frontend package. Instead of manually maintaining a frontend form schema that mirrors the backend DTO, the frontend just fetches this document and renders accordingly.
Frontend: @faster-crud/vue (v0.1.1)
The Vue package consumes the meta endpoint and provides a builder API for constructing typed form/table configurations.
import { fetchCrudMeta, field } from '@faster-crud/vue';
const userMeta = await fetchCrudMeta('https://api.example.com/users');
const formConfig = userMeta
.field('name').label('Full Name').type('text').required().rules([
{ min: 2, message: 'Name too short' }
])
.field('email').label('Email').type('email').required()
.field('role').label('Role').type('select').options(['admin', 'user'])
.build();
The builder chain is typed against the field names returned by fetchCrudMeta. Referencing a non-existent field is a TypeScript error.
formConfig can then be passed to a generic <CrudTable> or <CrudForm> Vue component that renders the UI. This is intentionally presentation-agnostic at the config level — you can swap in your own components.
Current Status
- Phase 1: Core + NestJS integration — complete.
defineCrud, DTO generation, TypeORM adapter, Swagger, tests. - Phase 2: Frontend meta consumer — complete.
@faster-crud/vuewithfetchCrudMetaand builder chain. - Phase 3: Codegen CLI — deferred. The original plan was a CLI that would read
defineCrudcalls in a codebase and emit static TypeScript client code (no runtime meta fetch needed). This is still a good idea, but Phase 1+2 are already useful without it.
Why Not Just Use an Existing Solution?
There are existing CRUD generators for NestJS — @nestjsx/crud is the most well-known. I looked at it seriously before starting.
The issues I ran into:
@nestjsx/crudis tied to TypeORM in a way that makes swapping adapters non-trivial- The DTO generation isn’t type-safe at the call site (field names are strings, not
keyof T) - No built-in frontend meta bridge — you still manually maintain the frontend schema
@faster-crud is smaller and more opinionated, but the things it does, it does with full type safety end-to-end.
What I’d Do Differently
The biggest architectural regret is that @faster-crud/nest’s dynamic DTO class generation is somewhat fragile. NestJS’s ValidationPipe and Swagger both rely on class metadata decorators being present at class definition time, not at runtime. I worked around this by calling Reflect.defineMetadata after class construction — it works, but it’s load-bearing magic that could break with NestJS internal changes.
If I were starting fresh, I’d look at using zod schemas as the source of truth (instead of entity key introspection) and generating both the runtime validation and the Swagger schema from that. The @anatine/zod-openapi approach is cleaner.
The code is on GitHub. PRs welcome, especially around the Prisma adapter and the codegen CLI.