Basic CRUD Recipe

Generate a complete CRUD system from a Prisma model in minutes.

Goal

Create a fully functional blog post management system with:

  • REST API endpoints
  • Validated DTOs
  • Admin dashboard pages
  • Search and pagination

Prerequisites

  • TGraph Backend Generator installed
  • NestJS project initialized
  • Prisma configured

Step 1: Define the Model

Add to prisma/schema.prisma:

// @tg_label(title)
// @tg_form()
model Post {
  id          String    @id @default(uuid())
  title       String
  content     String
  published   Boolean   @default(false)
  publishedAt DateTime?
  viewCount   Int       @default(0)
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt

  @@index([published, publishedAt])
}

Step 2: Run Migration

Apply the schema to your database:

npx prisma migrate dev --name add_post

Step 3: Generate Code

Run the generator:

tgraph all

Step 4: What You Get

Backend Files

Controller (post.tg.controller.ts):

@Controller('tg-api/posts')
@UseGuards(JwtAuthGuard, AdminGuard)
export class PostTgController {
  constructor(private readonly postService: PostTgService) {}

  @Get()
  async findAll(@Query() query: PaginationSearchDto) {
    return this.postService.findAll(query);
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    return this.postService.findOne(id);
  }

  @Post()
  async create(@Body() dto: CreatePostTgDto) {
    return this.postService.create(dto);
  }

  @Put(':id')
  async update(@Param('id') id: string, @Body() dto: UpdatePostTgDto) {
    return this.postService.update(id, dto);
  }

  @Delete(':id')
  async remove(@Param('id') id: string) {
    return this.postService.remove(id);
  }
}

Create DTO (create-post.tg.dto.ts):

export class CreatePostTgDto {
  @IsString()
  @IsNotEmpty()
  title: string;

  @IsString()
  @IsNotEmpty()
  content: string;

  @IsBoolean()
  @IsOptional()
  published?: boolean;

  @IsDate()
  @Type(() => Date)
  @IsOptional()
  publishedAt?: Date;

  @IsInt()
  @IsOptional()
  viewCount?: number;
}

Update DTO (update-post.tg.dto.ts):

export class UpdatePostTgDto {
  @IsString()
  @IsOptional()
  title?: string;

  @IsString()
  @IsOptional()
  content?: string;

  @IsBoolean()
  @IsOptional()
  published?: boolean;

  @IsDate()
  @Type(() => Date)
  @IsOptional()
  publishedAt?: Date;

  @IsInt()
  @IsOptional()
  viewCount?: number;
}

Service (post.tg.service.ts):

@Injectable()
export class PostTgService {
  constructor(private readonly prisma: PrismaService) {}

  async create(dto: CreatePostTgDto): Promise<Post> {
    return this.prisma.post.create({ data: dto });
  }

  async findAll(query: PaginationSearchDto): Promise<PaginatedResponse<Post>> {
    const { page = 1, limit = 10, search, sortBy, sortOrder } = query;
    const skip = (page - 1) * limit;

    const where: Prisma.PostWhereInput = search
      ? {
          OR: [
            { title: { contains: search, mode: 'insensitive' } },
            { content: { contains: search, mode: 'insensitive' } },
          ],
        }
      : {};

    const [data, total] = await Promise.all([
      this.prisma.post.findMany({
        where,
        skip,
        take: limit,
        orderBy: { [sortBy || 'createdAt']: sortOrder || 'desc' },
      }),
      this.prisma.post.count({ where }),
    ]);

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async findOne(id: string): Promise<Post> {
    const post = await this.prisma.post.findUnique({ where: { id } });
    if (!post) {
      throw new NotFoundException(`Post with ID ${id} not found`);
    }
    return post;
  }

  async update(id: string, dto: UpdatePostTgDto): Promise<Post> {
    await this.findOne(id);
    return this.prisma.post.update({
      where: { id },
      data: dto,
    });
  }

  async remove(id: string): Promise<Post> {
    await this.findOne(id);
    return this.prisma.post.delete({ where: { id } });
  }
}

Dashboard Files

List (PostList.tsx):

export const PostList = () => (
  <List>
    <Datagrid rowClick="edit">
      <TextField source="title" />
      <TextField source="content" />
      <BooleanField source="published" />
      <DateField source="publishedAt" />
      <NumberField source="viewCount" />
      <DateField source="createdAt" />
      <EditButton />
    </Datagrid>
  </List>
);

Edit (PostEdit.tsx):

export const PostEdit = () => (
  <Edit>
    <SimpleForm>
      <TextInput source="title" required />
      <TextInput source="content" multiline required />
      <BooleanInput source="published" />
      <DateTimeInput source="publishedAt" />
      <NumberInput source="viewCount" />
    </SimpleForm>
  </Edit>
);

Create (PostCreate.tsx):

export const PostCreate = () => (
  <Create>
    <SimpleForm>
      <TextInput source="title" required />
      <TextInput source="content" multiline required />
      <BooleanInput source="published" />
      <DateTimeInput source="publishedAt" />
      <NumberInput source="viewCount" />
    </SimpleForm>
  </Create>
);

Step 5: Test the API

Start your server:

npm run start:dev

Create a Post

curl -X POST http://localhost:3000/tg-api/posts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -d '{
    "title": "My First Post",
    "content": "This is the content of my first post.",
    "published": true
  }'

List Posts

curl http://localhost:3000/tg-api/posts?page=1&limit=10 \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Response:

{
  "data": [
    {
      "id": "uuid-here",
      "title": "My First Post",
      "content": "This is the content...",
      "published": true,
      "publishedAt": null,
      "viewCount": 0,
      "createdAt": "2025-01-01T00:00:00.000Z",
      "updatedAt": "2025-01-01T00:00:00.000Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 10,
  "totalPages": 1
}

Search Posts

curl "http://localhost:3000/tg-api/posts?search=first&page=1&limit=10" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Get Single Post

curl http://localhost:3000/tg-api/posts/uuid-here \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Update Post

curl -X PUT http://localhost:3000/tg-api/posts/uuid-here \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -d '{
    "viewCount": 100
  }'

Delete Post

curl -X DELETE http://localhost:3000/tg-api/posts/uuid-here \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Step 6: Test the Dashboard

Start your dashboard:

cd src/dashboard
npm run dev

Navigate to http://localhost:5173/posts

You can:

  • View all posts in a table
  • Search posts by title or content
  • Sort by any column
  • Click a row to edit
  • Click “Create” to add a new post
  • Click “Delete” to remove a post

Step 7: Add Validation

Enhance your model with validation:

// @tg_label(title)
// @tg_form()
model Post {
  id          String    @id @default(uuid())
  title       String    // @min(5) @max(200)
  content     String    // @min(20)
  published   Boolean   @default(false)
  publishedAt DateTime?
  viewCount   Int       @default(0)
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
}

Regenerate:

tgraph dtos

Now the DTOs include validation:

export class CreatePostTgDto {
  @IsString()
  @MinLength(5)
  @MaxLength(200)
  @IsNotEmpty()
  title: string;

  @IsString()
  @MinLength(20)
  @IsNotEmpty()
  content: string;

  // ...
}

Customization Examples

Add Custom Endpoint

Create post.service.ts:

import { PostTgService } from './post.tg.service';

@Injectable()
export class PostService extends PostTgService {
  async publish(id: string): Promise<Post> {
    return this.prisma.post.update({
      where: { id },
      data: {
        published: true,
        publishedAt: new Date(),
      },
    });
  }

  async incrementViewCount(id: string): Promise<Post> {
    return this.prisma.post.update({
      where: { id },
      data: {
        viewCount: { increment: 1 },
      },
    });
  }

  async getPublishedPosts(): Promise<Post[]> {
    return this.prisma.post.findMany({
      where: { published: true },
      orderBy: { publishedAt: 'desc' },
    });
  }
}

Create post.controller.ts:

import { PostService } from './post.service';

@Controller('posts')
@UseGuards(JwtAuthGuard)
export class PostController {
  constructor(private readonly postService: PostService) {}

  @Get('published')
  async getPublished() {
    return this.postService.getPublishedPosts();
  }

  @Put(':id/publish')
  @UseGuards(AdminGuard)
  async publish(@Param('id') id: string) {
    return this.postService.publish(id);
  }

  @Put(':id/view')
  async incrementView(@Param('id') id: string) {
    return this.postService.incrementViewCount(id);
  }
}

Update post.module.ts:

@Module({
  imports: [PrismaModule],
  controllers: [PostTgController, PostController],
  providers: [PostTgService, PostService],
  exports: [PostService],
})
export class PostModule {}

Customize Dashboard List

Create PostListCustom.tsx:

export const PostListCustom = () => {
  const filters = [<TextInput source="q" label="Search" alwaysOn />, <BooleanInput source="published" />];

  return (
    <List filters={filters}>
      <Datagrid rowClick="edit">
        <TextField source="title" />
        <FunctionField label="Preview" render={(record: any) => record.content?.substring(0, 100) + '...'} />
        <BooleanField source="published" />
        <NumberField source="viewCount" />
        <DateField source="createdAt" showTime />
        <EditButton />
        <ShowButton />
      </Datagrid>
    </List>
  );
};

Next Steps