TGraph Backend Generator - LLM Integration Guide
Version: 0.0.3
Purpose: This documentation is specifically designed for Large Language Models (LLMs) to understand and correctly use the @tgraph/backend-generator library.
Target: AI assistants helping developers implement full-stack applications with NestJS and React Admin.
LIBRARY OVERVIEW
What this library does:
- Generates complete NestJS backend APIs (controllers, services, DTOs) from Prisma schema files
- Generates complete React Admin dashboard pages (List, Edit, Create, Show, Studio)
- Provides static infrastructure files (guards, interceptors, decorators, utilities)
- Supports custom endpoints through adapters
- Maintains type safety from database to frontend
Core principle:
- Generated files use
.tg.naming convention (e.g.,user.tg.controller.ts,user.tg.service.ts) - Custom user code lives in separate files without
.tg.(e.g.,user.service.ts,user.controller.ts) - Generated files can be safely regenerated; custom files are never touched
INSTALLATION
Step 1: Install Package
npm install --save-dev @tgraph/backend-generator
Required peer dependencies:
{
"@nestjs/common": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.0.0",
"@types/express": "^4.17.0",
"@types/multer": "^1.4.0",
"express": "^4.18.0",
"multer": "^1.4.5-lts.1"
}
Step 2: Initialize Configuration
npx tgraph init
This creates tgraph.config.ts in the project root.
CRITICAL: All commands require this configuration file to exist.
CONFIGURATION FILE STRUCTURE
The configuration file (tgraph.config.ts) has the following structure:
import type { Config } from '@tgraph/backend-generator';
export const config: Config = {
input: {
prisma: {
schemaPath: 'prisma/schema.prisma',
servicePath: 'src/infrastructure/database/prisma.service.ts',
},
dashboard: {
components: { form: {}, display: {} },
},
},
output: {
backend: {
root: 'src/features',
dtosPath: 'src/dtos/generated',
modulesPaths: ['src/features', 'src/modules', 'src'],
guardsPath: 'src/guards',
decoratorsPath: 'src/decorators',
interceptorsPath: 'src/interceptors',
utilsPath: 'src/utils',
appModulePath: 'src/app.module.ts',
},
dashboard: {
enabled: true,
updateDataProvider: true,
root: 'src/dashboard/src',
resourcesPath: 'src/dashboard/src/resources',
swaggerJsonPath: 'src/dashboard/src/types/swagger.json',
apiPath: 'src/dashboard/src/types/api.ts',
appComponentPath: 'src/dashboard/src/App.tsx',
dataProviderPath: 'src/dashboard/src/providers/dataProvider.ts',
},
},
api: {
suffix: '',
prefix: 'tg-api',
authenticationEnabled: true,
requireAdmin: true,
guards: [{ name: 'JwtAuthGuard', importPath: '@/guards/jwt-auth.guard' }],
adminGuards: [{ name: 'AdminGuard', importPath: '@/guards/admin.guard' }],
},
behavior: {
nonInteractive: false,
},
};
CLI COMMANDS REFERENCE
Essential Commands
# Initialize configuration (MUST be run first)
npx tgraph init
# Check environment and configuration
npx tgraph doctor
# Preview changes without writing files
npx tgraph preflight
# Generate everything (API + Dashboard + DTOs)
npx tgraph all
# Generate only backend API
npx tgraph api
# Generate only dashboard
npx tgraph dashboard
# Generate only DTOs
npx tgraph dtos
# Generate static infrastructure files
npx tgraph static
# Generate dashboard API types from Swagger
npx tgraph types
# Generate Swagger JSON only
npx tgraph swagger
Command Options
Global flags (available on most commands):
--config <path>- Use a custom config file (default:tgraph.config.ts)-y, --yes, --non-interactive- Skip all prompts-h, --help- Display help output
API / Dashboard / All commands:
--public- Temporarily generate controllers without authentication guards (mirrorsapi.authenticationEnabled = false)
DTO command:
--config <path>-y, --yes, --non-interactive-h, --help
Init command:
--output <path>- Custom file name (default:tgraph.config.ts)--requireAdmin <true|false>- Whether the scaffolded config should enable admin guards-y, --yes, --non-interactive-h, --help
Static command:
--list- List available static modules--include <names>- Generate specific modules (comma-separated)--cat <name>- Preview a static file--non-interactive,-y,--yes-h, --help
Types command:
--skip-swagger- Skip running the configured Swagger command-h, --help
Doctor, preflight, adapters, swagger: support -h, --help only.
PRISMA SCHEMA ANNOTATION
Model-Level Directives
Mark models for generation:
// @tg_form()
model User {
id String @id @default(uuid())
name String
email String @unique
}
CRITICAL RULES:
- Only models with
// @tg_form()are processed - Must be double-slash comment (
//), not triple-slash (///) - Must appear immediately before the
modelkeyword - Must have exact syntax:
@tg_form()
Set display label for relations:
// @tg_label(name)
// @tg_form()
model User {
id String @id @default(uuid())
name String
email String @unique
}
Label field fallback order (if no @tg_label specified):
nametitleemailcodeslug- First non-ID string field
id(final fallback)
Field-Level Directives
CRITICAL: Field directives use triple-slash comments (///) and must appear immediately above the field.
Available field directives:
1. @tg_format
Controls string formatting and validation.
model User {
/// @tg_format(email)
email String @unique
/// @tg_format(url)
website String?
/// @tg_format(password)
password String
/// @tg_format(tel)
phone String?
}
Supported formats:
email- Email validation, generates@IsEmail()decoratorurl- URL validation, generates@IsUrl()decoratorpassword- Password input (hidden), generates@MinLength(8)by defaulttel- Telephone validation, generates phone regex pattern
Generated DTO example:
export class CreateUserTgDto {
@IsEmail()
@IsNotEmpty()
email: string;
@IsUrl()
@IsOptional()
website?: string;
@IsString()
@MinLength(8)
@IsNotEmpty()
password: string;
@Matches(/^[0-9+()\s-]+$/)
@IsOptional()
phone?: string;
}
Generated React Admin input:
<TextInput source="email" type="email" required />
<TextInput source="website" type="url" />
<TextInput source="password" type="password" required />
<TextInput source="phone" type="tel" />
2. @tg_upload
Enables file upload for string fields.
model Product {
/// @tg_upload(image)
thumbnail String?
/// @tg_upload(file)
datasheet String?
}
Supported types:
image- Image files with preview, generatesaccept="image/*"file- Any file type
Generated React Admin input:
<FileInput source="thumbnail" accept="image/*">
<ImageField source="src" title="title" />
</FileInput>
<FileInput source="datasheet">
<FileField source="src" title="title" />
</FileInput>
Upload workflow:
- User selects file in dashboard
- Data provider detects File object
- Uploads to
POST /uploadendpoint - Receives URL in response
- Replaces File object with URL string
- Submits to model API with URL string
CRITICAL: Backend must implement POST /upload endpoint:
@Controller('upload')
export class UploadController {
@Post()
@UseInterceptors(FileInterceptor('file'))
async upload(@UploadedFile() file: Express.Multer.File) {
// Save file and return URL
return { url: `/uploads/${file.filename}` };
}
}
3. @tg_readonly
Marks field as read-only (displayed but not editable).
model User {
/// @tg_readonly
ipAddress String?
/// @tg_readonly
userAgent String?
}
Generated React Admin input:
<TextInput source="ipAddress" disabled readOnly />
<TextInput source="userAgent" disabled readOnly />
Data provider behavior:
- Read-only fields are automatically stripped from create/update payloads
- Prevents accidental modification of system-managed fields
Inline Validation Comments
Add validation through inline comments:
model User {
username String // @min(3) @max(20)
email String @unique // @pattern(/^[^\s@]+@/)
bio String? // @max(500)
age Int // @min(18) @max(120)
password String // @min(8, [create])
}
Supported validation directives:
@min(n)- Minimum length/value@max(n)- Maximum length/value@length(n)- Exact length@pattern(/regex/)- Regex pattern
Operation-specific validation:
field String // @min(8, [create])
field String // @max(100, [create, update])
Valid operations: create, update
Generated DTO:
export class CreateUserTgDto {
@IsString()
@MinLength(3)
@MaxLength(20)
@IsNotEmpty()
username: string;
@IsEmail()
@Matches(/^[^\s@]+@/)
@IsNotEmpty()
email: string;
@IsString()
@MaxLength(500)
@IsOptional()
bio?: string;
@IsInt()
@Min(18)
@Max(120)
@IsNotEmpty()
age: number;
@IsString()
@MinLength(8)
@IsNotEmpty()
password: string;
}
Field Type Mapping
String types:
name String -> @IsString() @IsNotEmpty()
name String? -> @IsString() @IsOptional()
name String @default("value") -> @IsString() @IsOptional()
Number types:
age Int -> @IsInt() @IsNotEmpty()
price Float -> @IsNumber() @IsNotEmpty()
age Int? -> @IsInt() @IsOptional()
Boolean types:
active Boolean -> @IsBoolean() @IsNotEmpty()
active Boolean? -> @IsBoolean() @IsOptional()
active Boolean @default(true) -> @IsBoolean() @IsOptional()
DateTime types:
createdAt DateTime -> @IsDate() @IsNotEmpty()
createdAt DateTime? -> @IsDate() @IsOptional()
createdAt DateTime @default(now()) -> Excluded from Create DTO
updatedAt DateTime @updatedAt -> Excluded from DTOs
Enum types:
enum Role {
USER
ADMIN
}
role Role -> @IsEnum(Role) @IsNotEmpty()
role Role? -> @IsEnum(Role) @IsOptional()
role Role @default(USER) -> @IsEnum(Role) @IsOptional()
JSON types:
metadata Json -> metadata: any (no validation)
metadata Json? -> metadata?: any
Array types:
tags String[] -> @IsArray() @IsString({ each: true }) @IsNotEmpty()
tags String[]? -> @IsArray() @IsString({ each: true }) @IsOptional()
Relation types:
author User @relation(fields: [authorId], references: [id])
authorId String -> @IsString() @IsNotEmpty()
Relation Handling
One-to-many:
// @tg_label(name)
// @tg_form()
model User {
id String @id @default(uuid())
name String
posts Post[]
}
// @tg_form()
model Post {
id String @id @default(uuid())
title String
author User @relation(fields: [authorId], references: [id])
authorId String
@@index([authorId])
}
Generated React Admin input:
<ReferenceInput source="authorId" reference="users">
<AutocompleteInput optionText="name" />
</ReferenceInput>
Many-to-many:
// @tg_form()
model Post {
id String @id @default(uuid())
title String
categories Category[]
}
// @tg_form()
model Category {
id String @id @default(uuid())
name String
posts Post[]
}
Generated React Admin input:
<ReferenceArrayInput source="categoryIds" reference="categories">
<AutocompleteArrayInput optionText="name" />
</ReferenceArrayInput>
GENERATED FILE STRUCTURE
Backend Files
For each model marked with @tg_form(), the following files are generated:
Controller: src/features/{model-name}/{model-name}.tg.controller.ts
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { UserTgService } from './user.tg.service';
import { CreateUserTgDto } from './create-user.tg.dto';
import { UpdateUserTgDto } from './update-user.tg.dto';
import { JwtAuthGuard } from '@/guards/jwt-auth.guard';
import { AdminGuard } from '@/guards/admin.guard';
import { IsAdmin } from '@/decorators/is-admin.decorator';
@Controller('tg-api/users')
@UseGuards(JwtAuthGuard, AdminGuard)
@IsAdmin()
@ApiTags('users')
@ApiBearerAuth()
export class UserTgController {
constructor(private readonly userTgService: UserTgService) {}
@Get()
async findAll(@Query() query: PaginationSearchDto) {
return this.userTgService.findAll(query);
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.userTgService.findOne(id);
}
@Post()
async create(@Body() dto: CreateUserTgDto) {
return this.userTgService.create(dto);
}
@Put(':id')
async update(@Param('id') id: string, @Body() dto: UpdateUserTgDto) {
return this.userTgService.update(id, dto);
}
@Delete(':id')
async remove(@Param('id') id: string) {
return this.userTgService.remove(id);
}
}
Service: src/features/{model-name}/{model-name}.tg.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { CreateUserTgDto } from './create-user.tg.dto';
import { UpdateUserTgDto } from './update-user.tg.dto';
@Injectable()
export class UserTgService {
constructor(private readonly prisma: PrismaService) {}
private getSelectFields() {
return {
id: true,
name: true,
email: true,
// ... all fields except relations (unless configured)
};
}
async findAll(query: any) {
const { page = 1, limit = 10, search } = query;
const where = search ? {
OR: [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
],
} : {};
const [data, total] = await Promise.all([
this.prisma.user.findMany({
where,
select: this.getSelectFields(),
skip: (page - 1) * limit,
take: limit,
}),
this.prisma.user.count({ where }),
]);
return { data, total, page, limit };
}
async findOne(id: string) {
const item = await this.prisma.user.findUnique({
where: { id },
select: this.getSelectFields(),
});
if (!item) {
throw new NotFoundException('User not found');
}
return item;
}
// Auto-generated unique field getter
async getOneByEmail(email: string) {
const item = await this.prisma.user.findUnique({
where: { email },
select: this.getSelectFields(),
});
if (!item) {
throw new NotFoundException('User not found');
}
return item;
}
async create(dto: CreateUserTgDto) {
try {
return await this.prisma.user.create({
data: dto,
select: this.getSelectFields(),
});
} catch (error) {
if (error.code === 'P2002') {
throw new ConflictException('User with this email already exists');
}
throw error;
}
}
async update(id: string, dto: UpdateUserTgDto) {
await this.findOne(id); // Verify exists
try {
return await this.prisma.user.update({
where: { id },
data: dto,
select: this.getSelectFields(),
});
} catch (error) {
if (error.code === 'P2002') {
throw new ConflictException('User with this email already exists');
}
throw error;
}
}
async remove(id: string) {
await this.findOne(id); // Verify exists
return await this.prisma.user.delete({
where: { id },
select: this.getSelectFields(),
});
}
}
Create DTO: src/features/{model-name}/create-{model-name}.tg.dto.ts
import { IsString, IsEmail, IsOptional, IsNotEmpty } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateUserTgDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty()
@IsEmail()
@IsNotEmpty()
email: string;
@ApiPropertyOptional()
@IsString()
@IsOptional()
bio?: string;
}
Update DTO: src/features/{model-name}/update-{model-name}.tg.dto.ts
import { IsString, IsEmail, IsOptional } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class UpdateUserTgDto {
@ApiPropertyOptional()
@IsString()
@IsOptional()
name?: string;
@ApiPropertyOptional()
@IsEmail()
@IsOptional()
email?: string;
@ApiPropertyOptional()
@IsString()
@IsOptional()
bio?: string;
}
Module: src/features/{model-name}/{model-name}.module.ts (if doesn’t exist)
import { Module } from '@nestjs/common';
import { UserTgController } from './user.tg.controller';
import { UserTgService } from './user.tg.service';
@Module({
controllers: [UserTgController],
providers: [UserTgService],
exports: [UserTgService],
})
export class UserModule {}
Dashboard Files
For each model marked with @tg_form(), the following React Admin files are generated:
List: src/dashboard/src/resources/{model-name-plural}/{ModelName}List.tsx
import { List, Datagrid, TextField, EmailField, DateField, EditButton } from 'react-admin';
export const UserList = () => (
<List>
<Datagrid rowClick="edit">
<TextField source="name" />
<EmailField source="email" />
<DateField source="createdAt" />
<EditButton />
</Datagrid>
</List>
);
Edit: src/dashboard/src/resources/{model-name-plural}/{ModelName}Edit.tsx
import { Edit, SimpleForm, TextInput } from 'react-admin';
export const UserEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="name" required />
<TextInput source="email" type="email" required />
<TextInput source="bio" multiline />
</SimpleForm>
</Edit>
);
Create: src/dashboard/src/resources/{model-name-plural}/{ModelName}Create.tsx
import { Create, SimpleForm, TextInput } from 'react-admin';
export const UserCreate = () => (
<Create>
<SimpleForm>
<TextInput source="name" required />
<TextInput source="email" type="email" required />
<TextInput source="bio" multiline />
</SimpleForm>
</Create>
);
Show: src/dashboard/src/resources/{model-name-plural}/{ModelName}Show.tsx
import { Show, SimpleShowLayout, TextField, EmailField, DateField } from 'react-admin';
export const UserShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="name" />
<EmailField source="email" />
<TextField source="bio" />
<DateField source="createdAt" />
</SimpleShowLayout>
</Show>
);
Studio: src/dashboard/src/resources/{model-name-plural}/{ModelName}Studio.tsx
import { DatagridAGGrid } from '@react-admin/ra-datagrid-ag-grid';
import { List } from 'react-admin';
export const UserStudio = () => (
<List>
<DatagridAGGrid
columnDefs={[
{ field: 'name', editable: true },
{ field: 'email', editable: true },
{ field: 'bio', editable: true },
]}
/>
</List>
);
Index: src/dashboard/src/resources/{model-name-plural}/index.ts
export { UserList } from './UserList';
export { UserEdit } from './UserEdit';
export { UserCreate } from './UserCreate';
export { UserShow } from './UserShow';
export { UserStudio } from './UserStudio';
App.tsx Updates
The dashboard App.tsx is automatically updated with resource registrations:
import { Admin, Resource } from 'react-admin';
import { UserList, UserEdit, UserCreate, UserShow } from './resources/users';
import dataProvider from './providers/dataProvider';
const App = () => (
<Admin dataProvider={dataProvider}>
<Resource
name="users"
list={UserList}
edit={UserEdit}
create={UserCreate}
show={UserShow}
/>
</Admin>
);
export default App;
Field Directives Metadata
Field directives are exported to dashboard runtime:
File: src/dashboard/src/providers/fieldDirectives.generated.ts
export const fieldDirectives = {
users: {
email: { tgFormat: 'email' },
avatar: { tgUpload: 'image' },
ipAddress: { tgReadOnly: true },
},
posts: {
coverImage: { tgUpload: 'image' },
},
};
CUSTOM ADAPTERS
Overview
Adapters provide a way to create custom endpoints without modifying generated controller files. They live in separate files and are automatically discovered during generation.
Location: src/features/{model-name}/adapters/*.adapter.ts
Benefits:
- No editing of generated files
- Automatic discovery and integration
- Full type safety with generics
- Auto-generated DTOs and validation
- OpenAPI documentation generated automatically
Basic Adapter Structure
// src/features/post/adapters/create-with-slug.adapter.ts
import { adapter } from '@/adapters/runtime';
// Define request body type for type safety
interface CreatePostBody {
title: string;
content: string;
}
export default adapter.json<CreatePostBody>(
{
method: 'POST',
path: '/with-slug',
target: 'PostService.create',
auth: 'JwtAuthGuard',
},
async (ctx) => {
const { body, helpers } = ctx;
// body is typed as CreatePostBody
const slug = helpers.slugify(body.title);
return {
args: {
...body,
slug,
},
};
}
);
Adapter Types
JSON Adapter (most common):
export default adapter.json(
{
method: 'POST',
path: '/endpoint',
target: 'ServiceName.methodName',
},
async (ctx) => {
// ctx.body contains parsed JSON
return { args: { ...ctx.body } };
}
);
Multipart Adapter (for file uploads):
export default adapter.multipart(
{
method: 'POST',
path: '/upload-image',
target: 'PostService.update',
},
async (ctx) => {
const { files, body, helpers } = ctx;
const file = Array.isArray(files) ? files[0] : files;
const url = await helpers.upload.minio(file, 'images');
return {
args: {
id: body.id,
imageUrl: url,
},
};
}
);
Configuration Options
Required fields:
{
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
path: '/endpoint', // Must start with /
}
Optional fields:
{
target: 'ServiceName.methodName', // Service method to call (or null to bypass)
auth: 'JwtAuthGuard', // Single guard
auth: ['JwtAuthGuard', 'AdminGuard'], // Multiple guards
select: ['id', 'title'], // Fields to return
include: ['author'], // Relations to include
description: 'Description', // OpenAPI description
summary: 'Summary', // OpenAPI summary
tags: ['custom'], // OpenAPI tags
}
CRITICAL: Cannot use both select and include in the same adapter.
Context API
The adapter handler receives a context object:
async (ctx) => {
// Request data
ctx.url // Full request URL
ctx.params // Route parameters: { id: '123' }
ctx.query // Query string: { page: '1' }
ctx.body // Parsed JSON body
ctx.headers // Request headers
ctx.files // Uploaded files (multipart only)
// Authentication
ctx.user // Authenticated user from guard
// Dependencies
ctx.di.prisma // PrismaService instance
ctx.di.{service} // Other injected services
// Utilities
ctx.helpers.uuid() // Generate UUID v4
ctx.helpers.slugify(text) // Convert to slug
ctx.helpers.ext(filename) // Get file extension
ctx.helpers.pick(obj, keys) // Pick object properties
ctx.helpers.assert(condition, msg) // Assert condition
ctx.helpers.upload.minio(file) // Upload to MinIO (user-implemented)
// Raw objects (advanced)
ctx.req // Express Request
ctx.res // Express Response
}
TypeScript Generics for Type Safety
Provide explicit types for request body, query, and params:
interface CreatePostBody {
title: string;
content: string;
}
interface PostQuery {
published?: boolean;
page?: string;
}
interface PostParams {
id: string;
}
// Pass generic types: <TBody, TQuery, TParams>
export default adapter.json<CreatePostBody, PostQuery, PostParams>(
{
method: 'POST',
path: '/:id',
target: 'PostService.create',
},
async (ctx) => {
// Fully typed - no 'any'!
const title: string = ctx.body.title; // ✓ Typed
const published = ctx.query.published; // ✓ Typed
const id: string = ctx.params.id; // ✓ Typed
return { args: ctx.body };
}
);
Return Types
Call service method:
return {
args: {
title: 'My Title',
content: 'Content',
},
};
Direct response (bypass service):
return adapter.response(200, {
success: true,
message: 'Processed',
});
Direct response with headers:
return adapter.response(
201,
{ id: '123' },
{ 'X-Custom-Header': 'value' }
);
Common Patterns
External API integration:
export default adapter.json(
{
method: 'POST',
path: '/sync-stripe',
},
async (ctx) => {
const stripeCustomer = await stripe.customers.create({
email: ctx.body.email,
});
await ctx.di.prisma.user.update({
where: { id: ctx.body.userId },
data: { stripeCustomerId: stripeCustomer.id },
});
return adapter.response(200, {
success: true,
customerId: stripeCustomer.id,
});
}
);
Custom validation:
export default adapter.json(
{
method: 'POST',
path: '/validate',
target: 'UserService.create',
},
async (ctx) => {
const { body, helpers, di } = ctx;
helpers.assert(body.age >= 18, 'Must be 18 or older');
const existing = await di.prisma.user.findUnique({
where: { email: body.email },
});
helpers.assert(!existing, 'Email already registered');
return { args: body };
}
);
STATIC FILES
The generator can create static infrastructure files:
npx tgraph static --list
Available modules:
admin.guard- Admin role guardfeature-flag.guard- Feature flag guard stubis-admin.decorator- IsAdmin decoratorpaginated-search-query.dto- Pagination query DTOpaginated-search-result.dto- Pagination result DTOapi-response.dto- Standard API response wrapperpagination.interceptor- Pagination interceptoraudit.interceptor- Audit logging interceptor stubpaginated-search.decorator- Paginated search decoratorpaginated-search.util- Pagination utilitiesadapter.runtime- Adapter runtime factoryadapter.types- Adapter TypeScript typesadapter.helpers- Adapter helper utilitiesadapter.context- Adapter context buildergenerate.swagger- Swagger generation script template
Generate specific modules:
npx tgraph static --include admin.guard,pagination.interceptor
Generate all modules:
npx tgraph static --yes
TYPICAL WORKFLOW
Initial Setup
- Install package:
npm install --save-dev @tgraph/backend-generator - Initialize configuration:
npx tgraph init - Check environment:
npx tgraph doctor
Annotate Prisma Schema
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
// @tg_label(name)
// @tg_form()
model User {
id String @id @default(uuid())
name String // @min(2) @max(100)
/// @tg_format(email)
email String @unique
/// @tg_upload(image)
avatar String?
role Role @default(USER)
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// @tg_label(title)
// @tg_form()
model Post {
id String @id @default(uuid())
title String // @min(5) @max(200)
content String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
/// @tg_upload(image)
coverImage String?
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}
enum Role {
USER
AUTHOR
ADMIN
}
Generate Code
# Preview changes
npx tgraph preflight
# Generate everything
npx tgraph all
# Or generate selectively
npx tgraph api # Backend only
npx tgraph dashboard # Dashboard only
npx tgraph dtos # DTOs only
Generate Static Files
# Interactive selection
npx tgraph static
# Or generate specific modules
npx tgraph static --include admin.guard,pagination.interceptor,is-admin.decorator
Generate Dashboard Types
# Generate Swagger JSON and TypeScript API client
npx tgraph types
Test Generated API
# Start backend
npm run start:dev
# Test endpoints
curl -X GET http://localhost:3000/tg-api/users
curl -X POST http://localhost:3000/tg-api/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer TOKEN" \
-d '{"name":"John Doe","email":"john@example.com"}'
Extend with Custom Logic
Custom service:
// src/features/post/post.service.ts
import { Injectable } from '@nestjs/common';
import { PostTgService } from './post.tg.service';
@Injectable()
export class PostService extends PostTgService {
async publish(id: string) {
return this.prisma.post.update({
where: { id },
data: {
published: true,
publishedAt: new Date(),
},
});
}
}
Custom controller:
// src/features/post/post.controller.ts
import { Controller, Put, Param, UseGuards } from '@nestjs/common';
import { PostService } from './post.service';
import { JwtAuthGuard } from '@/guards/jwt-auth.guard';
@Controller('posts')
@UseGuards(JwtAuthGuard)
export class PostController {
constructor(private readonly postService: PostService) {}
@Put(':id/publish')
async publish(@Param('id') id: string) {
return this.postService.publish(id);
}
}
Custom adapter:
// src/features/post/adapters/publish.adapter.ts
import { adapter } from '@/adapters/runtime';
export default adapter.json(
{
method: 'PUT',
path: '/:id/publish',
auth: ['JwtAuthGuard', 'AdminGuard'],
description: 'Publish a post',
},
async (ctx) => {
const { params, di } = ctx;
const post = await di.prisma.post.update({
where: { id: params.id },
data: {
published: true,
publishedAt: new Date(),
},
});
return adapter.response(200, post);
}
);
Regenerate After Schema Changes
# Update Prisma schema
# Then regenerate
npx tgraph all
# Or regenerate specific parts
npx tgraph api
npx tgraph dashboard
COMMON PATTERNS AND BEST PRACTICES
Multiple API Configurations
Generate both admin and public APIs:
# Admin API with authentication
npx tgraph api --config tgraph.admin.config.ts
# Public API without authentication
npx tgraph api --public --config tgraph.public.config.ts
Set api.suffix inside each config file (for example Admin vs Public) to control class names and file suffixes.
CI/CD Integration
# Non-interactive mode for CI
npx tgraph all --yes
Monorepo Structure
Define a monorepo-specific config so every path stays accurate:
// tgraph.mono.config.ts
import type { Config } from '@tgraph/backend-generator';
import { config as base } from './tgraph.config';
export const config: Config = {
...base,
input: {
...base.input,
prisma: {
...base.input.prisma,
schemaPath: 'apps/api/prisma/schema.prisma',
servicePath: 'apps/api/src/infrastructure/database/prisma.service.ts',
},
},
output: {
backend: {
...base.output.backend,
root: 'apps/api/src/features',
dtosPath: 'apps/api/src/dtos/generated',
modulesPaths: ['apps/api/src/features', 'apps/api/src/modules'],
},
dashboard: {
...base.output.dashboard,
root: 'apps/admin/src',
resourcesPath: 'apps/admin/src/resources',
},
},
};
Run the generator with that config:
npx tgraph all --config tgraph.mono.config.ts
File Upload Implementation
Backend upload controller:
import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
@Controller('upload')
export class UploadController {
@Post()
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, file.fieldname + '-' + uniqueSuffix + extname(file.originalname));
},
}),
})
)
async upload(@UploadedFile() file: Express.Multer.File) {
return {
url: `/uploads/${file.filename}`,
filename: file.filename,
mimetype: file.mimetype,
size: file.size,
};
}
}
Unique Field Lookups
Generated services automatically include getters for unique fields:
// Prisma schema
model User {
email String @unique
phone String @unique
}
// Generated service methods
async getOneByEmail(email: string) {
const item = await this.prisma.user.findUnique({
where: { email },
select: this.getSelectFields(),
});
if (!item) {
throw new NotFoundException('User not found');
}
return item;
}
async getOneByPhone(phone: string) {
const item = await this.prisma.user.findUnique({
where: { phone },
select: this.getSelectFields(),
});
if (!item) {
throw new NotFoundException('User not found');
}
return item;
}
Relation Includes (Programmatic)
The CLI generates scalar selects by default. When you need relations in service responses, use the SDK and pass relationsInclude to NestServiceGenerator:
import { NestServiceGenerator } from '@tgraph/backend-generator';
const services = new NestServiceGenerator({
suffix: 'Admin',
relationsInclude: ['author', 'comments'], // or 'all'
});
This instructs the generator to include relation selects with their inferred display fields.
ERROR HANDLING AND TROUBLESHOOTING
Common Issues
Issue: Command not found
Solution: Use npx prefix or install globally
npx tgraph all
OR
npm install -g @tgraph/backend-generator
Issue: No configuration file found
Solution: Initialize configuration first
npx tgraph init
Issue: Schema not found
Solution: Verify `input.prisma.schemaPath` in your config (point it to the correct .prisma file) and rerun the generator
npx tgraph api --config tgraph.config.ts
Issue: Field directive not working
Solution: Ensure triple-slash comments (///) for field directives
Correct: /// @tg_format(email)
Incorrect: // @tg_format(email)
Issue: Model-level directive not working
Solution: Ensure double-slash comments (//) for model directives
Correct: // @tg_form()
Incorrect: /// @tg_form()
Issue: Adapter not discovered
Solution: Verify file location and naming
- Location: src/features/{model}/adapters/*.adapter.ts
- Naming: must end with .adapter.ts
- Export: must use 'export default'
Issue: Type errors in generated files
Solution: Install required peer dependencies
npm install @nestjs/common @nestjs/swagger @types/express @types/multer
CRITICAL RULES SUMMARY
- Model directives use double-slash (
//)// @tg_form()// @tg_label(name)
- Field directives use triple-slash (
///)/// @tg_format(email)/// @tg_upload(image)/// @tg_readonly
- Inline validation uses double-slash (
//)field String // @min(3) @max(20)
- Generated files use
.tg.naminguser.tg.controller.tsuser.tg.service.tscreate-user.tg.dto.ts
- Custom files never use
.tg.naminguser.service.tsuser.controller.ts
- Configuration file must exist
- Run
tgraph initfirst - Default name:
tgraph.config.ts
- Run
- Adapters must be in correct location
- Path:
{modulePath}/adapters/*.adapter.ts - Must export default
- Path:
- Upload endpoint must be implemented
- Required for
@tg_uploaddirective - Must respond with
{ url: string }
- Required for
- Cannot use both select and include
- In adapter config
- In service method calls
- All commands require configuration file
- Except
tgraph init - Except
tgraph --help
- Except
COMPLETE EXAMPLE
Here’s a complete example from start to finish:
1. Install and Initialize
npm install --save-dev @tgraph/backend-generator
npx tgraph init
npx tgraph doctor
2. Create Prisma Schema
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
// @tg_label(name)
// @tg_form()
model User {
id String @id @default(uuid())
name String // @min(2) @max(100)
/// @tg_format(email)
email String @unique
/// @tg_upload(image)
avatar String?
role Role @default(USER)
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// @tg_label(title)
// @tg_form()
model Post {
id String @id @default(uuid())
title String // @min(5) @max(200)
content String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
/// @tg_upload(image)
coverImage String?
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}
enum Role {
USER
AUTHOR
ADMIN
}
3. Generate Code
npx tgraph preflight # Preview
npx tgraph all # Generate
npx tgraph static # Generate infrastructure
npx tgraph types # Generate API client
4. Implement Upload Endpoint
// src/features/upload/upload.controller.ts
import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
@Controller('upload')
export class UploadController {
@Post()
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, file.fieldname + '-' + uniqueSuffix + extname(file.originalname));
},
}),
})
)
async upload(@UploadedFile() file: Express.Multer.File) {
return {
url: `/uploads/${file.filename}`,
};
}
}
5. Create Custom Adapter
// src/features/post/adapters/publish.adapter.ts
import { adapter } from '@/adapters/runtime';
export default adapter.json(
{
method: 'PUT',
path: '/:id/publish',
auth: ['JwtAuthGuard', 'AdminGuard'],
description: 'Publish a post and set publish date',
},
async (ctx) => {
const { params, di, helpers } = ctx;
const post = await di.prisma.post.findUnique({
where: { id: params.id },
});
helpers.assert(post, 'Post not found');
helpers.assert(!post.published, 'Post already published');
const updated = await di.prisma.post.update({
where: { id: params.id },
data: {
published: true,
publishedAt: new Date(),
},
});
return adapter.response(200, {
success: true,
post: updated,
});
}
);
6. Regenerate to Include Adapter
npx tgraph api
7. Test API
# Start server
npm run start:dev
# Get all users
curl http://localhost:3000/tg-api/users \
-H "Authorization: Bearer TOKEN"
# Create user
curl -X POST http://localhost:3000/tg-api/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer TOKEN" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"role": "AUTHOR"
}'
# Publish post
curl -X PUT http://localhost:3000/tg-api/posts/POST_ID/publish \
-H "Authorization: Bearer TOKEN"
8. Start Dashboard
cd src/dashboard
npm run dev
Open http://localhost:5173 and test the dashboard.
VERSION INFORMATION
Current version: 0.0.3
Node.js requirement: >= 18.0.0
NPM requirement: >= 9.0.0
Peer dependencies:
- @nestjs/common: ^10.0.0
- @nestjs/platform-express: ^10.0.0
- @nestjs/swagger: ^7.0.0
- @types/express: ^4.17.0
- @types/multer: ^1.4.0
- express: ^4.18.0
- multer: ^1.4.5-lts.1
ADDITIONAL RESOURCES
GitHub Repository: https://github.com/trugraph/backend-generator
NPM Package: https://www.npmjs.com/package/@tgraph/backend-generator
Documentation Site: https://trugraph.github.io/backend-generator/
Issues: https://github.com/trugraph/backend-generator/issues
END OF GUIDE
This guide provides complete information for LLMs to understand and correctly use the @tgraph/backend-generator library. All syntax, patterns, and workflows have been documented with precision for machine parsing and understanding.