End-to-end type-safe CRUD for any framework
Define your entity once with decorators — get a full REST API, validation, filtering, pagination, and auto-generated frontend UI across any backend and frontend framework.
| Package | Version | Description |
|---|---|---|
@faster-crud/core |
Framework-agnostic decorators & types | |
@faster-crud/nest |
NestJS integration | |
@faster-crud/typeorm |
TypeORM adapter | |
@faster-crud/prisma |
Prisma adapter | |
@faster-crud/drizzle |
Drizzle adapter | |
@faster-crud/mongoose |
Mongoose/MongoDB adapter | |
@faster-crud/mikro-orm |
MikroORM adapter | |
@faster-crud/hono |
Hono framework adapter | |
@faster-crud/express |
Express adapter | |
@faster-crud/fastify |
Fastify adapter | |
@faster-crud/react |
React hooks & components | |
@faster-crud/svelte |
Svelte 5 stores & components | |
@faster-crud/solid |
SolidJS stores & components | |
@faster-crud/trpc |
tRPC router builder | |
@faster-crud/graphql |
GraphQL resolver factory | |
@faster-crud/gen |
CLI code generator | |
@faster-crud/validation |
Validation middleware | |
@faster-crud/auth |
JWT auth helpers |
npm install @faster-crud/core @faster-crud/nest @faster-crud/typeorm typeorm @nestjs/typeorm reflect-metadataimport { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { Resource, Col, Rule, Deny } from '@faster-crud/core';
@Entity()
@Resource('users', { pagination: { max: 50 } })
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
@Col({ label: 'Username' })
@Rule.required()
@Rule.length(3, 20)
username: string;
@Column()
@Col({ label: 'Email' })
@Rule.email()
email: string;
@Column()
@Col({ label: 'Role' })
@Deny('create')
role: string;
}import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TypeOrmResourceService } from '@faster-crud/typeorm';
import { User } from './user.entity';
@Injectable()
export class UserService extends TypeOrmResourceService(User) {
constructor(@InjectRepository(User) repo: Repository<User>) {
super(repo);
}
}import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NestCrudModule } from '@faster-crud/nest';
import { User } from './user.entity';
import { UserService } from './user.service';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
NestCrudModule.forFeature([{ resource: User, service: UserService }]),
],
})
export class UsersModule {}This gives you:
| Method | Path | Description |
|---|---|---|
POST |
/users |
Create |
GET |
/users |
List (with pagination & filters) |
GET |
/users/:id |
Get by ID |
PATCH |
/users/:id |
Update |
DELETE |
/users/:id |
Remove |
npm install @faster-crud/core @faster-crud/nest @faster-crud/prisma @prisma/client reflect-metadataimport { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { Col, Resource, Rule, Searchable } from '@faster-crud/core';
import { PrismaResourceService } from '@faster-crud/prisma';
const prisma = new PrismaClient();
@Resource('users', {
softDelete: true,
cache: { ttl: 5_000 },
})
export class User {
@Col()
id: number;
@Searchable()
@Col({ label: 'Name' })
@Rule.required()
name: string;
@Searchable()
@Col({ label: 'Email' })
@Rule.email()
email: string;
@Col()
deletedAt?: Date | null;
}
@Injectable()
export class UsersService extends PrismaResourceService(User, prisma.user) {}Include related records by passing Prisma include options directly:
await usersService.get(1, { include: { posts: true } });
await usersService.list(
{
page: { current: 1, size: 20 },
sort: { field: 'name', order: 'asc' },
filters: {
name: { op: 'like', value: 'ada' },
},
},
{ include: { profile: true } },
);npm install @faster-crud/core @faster-crud/graphql @nestjs/graphql graphql reflect-metadataimport { Resolver } from '@nestjs/graphql';
import { GraphqlCrudFactory, TypeOrmAdapter } from '@faster-crud/graphql';
import { User } from './user.entity';
import { CreateUserDto, UpdateUserDto } from './user.dto';
const UserResolverBase = GraphqlCrudFactory.create({
entity: User,
adapter: new TypeOrmAdapter(userRepository),
dto: {
create: CreateUserDto,
update: UpdateUserDto,
},
});
@Resolver(() => User)
export class UserResolver extends UserResolverBase {}Generated GraphQL operations:
| Type | Name |
|---|---|
Query |
users(query?) |
Query |
user(id) |
Mutation |
createUser(input) |
Mutation |
updateUser(id, input) |
Mutation |
deleteUser(id) |
For Prisma, replace TypeOrmAdapter with new PrismaAdapter(prisma.user).
List queries currently support page, size, sortField, and sortOrder arguments.
Scaffold a fully-wired CRUD resource in one command:
npx @faster-crud/gen add User --fields "name:string,email:string" --outdir src/resourcesThis generates:
src/resources/User/
User.entity.ts ← TypeORM entity with @Resource, @Col decorators
User.service.ts ← Injectable service extending TypeOrmResourceService
User.module.ts ← NestJS module with auto-generated controller
Supported field types: string, number, boolean, Date.
Override hooks in your service to run logic before/after CRUD operations:
@Injectable()
export class UserService extends TypeOrmResourceService(User) {
constructor(@InjectRepository(User) repo: Repository<User>) {
super(repo);
}
async onBeforeCreate(dto: Partial<User>): Promise<Partial<User>> {
dto.createdAt = new Date();
return dto;
}
async onAfterCreate(entity: User): Promise<void> {
await this.emailService.sendWelcome(entity.email);
}
async onBeforeUpdate(id: number, dto: Partial<User>): Promise<Partial<User>> {
dto.updatedAt = new Date();
return dto;
}
async onAfterUpdate(entity: User): Promise<void> { /* audit log */ }
async onBeforeRemove(id: number): Promise<void> { /* check references */ }
async onAfterRemove(id: number): Promise<void> { /* cleanup */ }
}All hooks have default no-op implementations — override only what you need.
For TypeORM, soft delete is auto-detected from @DeleteDateColumn():
import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm';
@Entity()
@Resource('posts')
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
@Col({ label: 'Title' })
title: string;
@DeleteDateColumn()
deletedAt?: Date; // ← enables soft delete automatically
}For Prisma and other adapters, enable it explicitly on the resource:
import { Col, Resource } from '@faster-crud/core';
@Resource('posts', { softDelete: true })
export class Post {
@Col()
id: number;
@Col()
title: string;
@Col()
deletedAt?: Date | null;
}When soft delete is enabled:
DELETE /posts/:idsetsdeletedAtinstead of removing the row- List/get queries automatically exclude soft-deleted records
Enable in-memory caching for list() and get() with @Resource(..., { cache }):
import { Col, Resource, Searchable } from '@faster-crud/core';
@Resource('users', {
cache: {
ttl: 10_000,
},
})
export class User {
@Col()
id: number;
@Searchable()
@Col()
name: string;
}const first = await usersService.list({ page: { current: 1, size: 20 } });
const second = await usersService.list({ page: { current: 1, size: 20 } });
// `second` is served from cache until TTL expires.
await usersService.update(1, { name: 'Ada Lovelace' });
// Writes invalidate cached list/get results automatically.
const fresh = await usersService.get(1);GET /users?filters[age][op]=gt&filters[age][value]=18
GET /users?filters[name][op]=like&filters[name][value]=john
GET /users?filters[status][op]=in&filters[status][value]=active,pending
| Operator | Description | Example |
|---|---|---|
eq |
Equals (default) | { op: 'eq', value: 5 } |
ne |
Not equals | { op: 'ne', value: 'admin' } |
gt |
Greater than | { op: 'gt', value: 18 } |
gte |
Greater than or equal | { op: 'gte', value: 18 } |
lt |
Less than | { op: 'lt', value: 100 } |
lte |
Less than or equal | { op: 'lte', value: 50 } |
like |
Substring match | { op: 'like', value: 'john' } |
in |
Value in array | { op: 'in', value: [1, 2, 3] } |
between |
Range (inclusive) | { op: 'between', value: [10, 100] } |
Programmatic usage:
const query: PageQuery<User> = {
filters: {
age: { op: 'gte', value: 18 },
name: { op: 'like', value: 'john' },
status: { op: 'in', value: ['active', 'pending'] },
},
};If @nestjs/swagger is installed, @ApiProperty() and @ApiOperation() decorators are applied automatically based on your @Col and @Rule metadata — zero manual Swagger config needed.
npm install @nestjs/swaggerWhat gets auto-generated:
@ApiProperty({ description, required, type })on every DTO field@ApiTags()from@Resourcename@ApiOperation()on each CRUD endpoint
If class-validator is installed, validation decorators are applied automatically from your @Rule metadata:
npm install class-validator class-transformer@Rule decorator |
Auto-applied validator |
|---|---|
@Rule.required() |
@IsNotEmpty() |
@Rule.email() |
@IsEmail() |
@Rule.length(min, max) |
@MinLength(min) + @MaxLength(max) |
@Rule.pattern(regex) |
@Matches(regex) |
| (optional field) | @IsOptional() |
Every CRUD resource exposes GET /__crud/meta for frontend introspection. This returns the full ResourceMeta including resource name, available operations, field definitions (labels, types, UI hints, validation rules), and pagination config — enabling frontends to auto-generate forms and tables.
Works on Bun, Deno, Cloudflare Workers, and Node.js.
npm install @faster-crud/hono honoimport { Hono } from 'hono';
import { HonoCrudRouter } from '@faster-crud/hono';
import { User } from './user.entity';
const app = new Hono();
app.route('/users', HonoCrudRouter(User, userService));
export default app;npm install @faster-crud/express expressimport express from 'express';
import { expressCrudRouter } from '@faster-crud/express';
import { User } from './user.entity';
const app = express();
app.use(express.json());
app.use('/api/users', expressCrudRouter(User, userService));
app.listen(3000);npm install @faster-crud/fastify fastifyimport Fastify from 'fastify';
import { FastifyCrudPlugin } from '@faster-crud/fastify';
import { User } from './user.entity';
const fastify = Fastify();
await fastify.register(FastifyCrudPlugin(User, userService), { prefix: '/users' });
fastify.listen({ port: 3000 });All adapters generate the same REST endpoints and accept any service object implementing create, list, get, update, and remove.
All frontend packages fetch ResourceMeta from the backend to auto-generate tables and forms.
npm install @faster-crud/vue<script setup lang="ts">
import { useCrud, CrudTable, CrudForm } from '@faster-crud/vue';
const crud = useCrud<User>('/api/users');
</script>
<template>
<CrudTable :crud="crud" @edit="onEdit" />
<CrudForm :crud="crud" mode="create" />
</template>npm install @faster-crud/reactimport { useCrud, CrudTable, CrudForm } from '@faster-crud/react';
function UsersPage() {
const crud = useCrud<User>('/api/users');
return (
<>
<CrudTable crud={crud} onEdit={setEditing} />
<CrudForm crud={crud} mode="create" onDone={() => crud.fetchList()} />
</>
);
}The useCrud hook returns reactive state for data, total, loading, page, filters, sort, and methods for fetchList, create, update, remove.
npm install @faster-crud/svelte<script>
import { createCrudStore } from '@faster-crud/svelte';
import CrudTable from '@faster-crud/svelte/src/CrudTable.svelte';
import CrudForm from '@faster-crud/svelte/src/CrudForm.svelte';
const store = createCrudStore('/api/users');
</script>
<CrudTable {store} onEdit={handleEdit} />
<CrudForm {store} mode="create" />| Decorator | Description |
|---|---|
@Resource(name, options?) |
Marks a class as a CRUD resource. name becomes the route path. Options: operations, pagination, guardTokens. |
| Decorator | Description |
|---|---|
@Col(options?) |
Marks a field as a CRUD column. Accepts label, ui, and list options. |
@Deny(...ops) |
Deny a field on specific operations ('create', 'update', etc.). |
@Readonly() |
Shorthand for @Deny('create', 'update'). |
@Hidden(...views) |
Omit field from responses on specified views ('list', 'get'). |
@Searchable() |
Mark field as searchable for query filtering. |
@Ignore() |
Completely exclude field from all CRUD operations. |
@AdminOnly(...ops) |
Mark field as admin-only for specified operations. |
| Decorator | Description |
|---|---|
@Rule.required(msg?) |
Field is required. |
@Rule.length(min, max, msg?) |
String length constraint. |
@Rule.range(min, max, msg?) |
Numeric range constraint. |
@Rule.email(msg?) |
Must be valid email format. |
@Rule.pattern(regex, msg?) |
Must match regex pattern. |
MIT