Code Generation
The Grit code generator creates full-stack CRUD resources from a single command. It generates 8 new files and injects code into 10 existing files, wiring everything together automatically so your resource is immediately usable.
How the Generator Works
When you run grit generate resource, the CLI goes through four stages:
Detect project root
Walks up from the current directory looking for docker-compose.yml or turbo.json. Reads the Go module path from apps/api/go.mod.
Parse definition
Reads the resource definition from --fields, --from (YAML), or -i (interactive). Validates all field names and types.
Generate new files
Creates 8 new files using string template replacement. Each template uses placeholders like {{Pascal}}, {{plural}}, {{MODULE}} that get replaced with the actual resource names.
Inject into existing files
Finds marker comments in existing files (e.g., // grit:models) and injects code at those locations. This wires up routes, imports, model registration, and more.
Files Created (8 New Files)
For a resource named "Post", the generator creates the following files. Each file is complete, ready to use, and follows Grit's conventions.
| File | Contains |
|---|---|
| apps/api/internal/models/post.go | GORM struct with all fields, tags, timestamps, soft delete |
| apps/api/internal/services/post.go | Business logic: List (paginated), GetByID, Create, Update, Delete |
| apps/api/internal/handlers/post.go | Gin handlers: List, GetByID, Create, Update, Delete with validation |
| packages/shared/schemas/post.ts | CreatePostSchema, UpdatePostSchema with Zod validators |
| packages/shared/types/post.ts | TypeScript interface with all fields, id, created_at, updated_at |
| apps/web/hooks/use-posts.ts | React Query hooks: usePosts, useGetPost, useCreatePost, useUpdatePost, useDeletePost |
| apps/admin/resources/posts.ts | Resource definition: columns, form fields, filters, dashboard widget |
| apps/admin/app/resources/posts/page.tsx | Admin page component that renders the resource using ResourcePage |
Generated Code Examples
Here is what the generator produces for grit g resource Post --fields "title:string,content:text,published:bool":
Go Model
package models
import (
"time"
"gorm.io/gorm"
)
// Post represents a post in the system.
type Post struct {
ID uint `gorm:"primarykey" json:"id"`
Title string `gorm:"size:255" json:"title" binding:"required"`
Content string `gorm:"type:text" json:"content"`
Published bool `json:"published"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}Zod Schema
import { z } from "zod";
export const CreatePostSchema = z.object({
title: z.string().min(1, "Required"),
content: z.string(),
published: z.boolean(),
});
export const UpdatePostSchema = z.object({
title: z.string().min(1, "Required").optional(),
content: z.string().optional(),
published: z.boolean().optional(),
});
export type CreatePostInput = z.infer<typeof CreatePostSchema>;
export type UpdatePostInput = z.infer<typeof UpdatePostSchema>;TypeScript Interface
export interface Post {
id: number;
title: string;
content: string;
published: boolean;
created_at: string;
updated_at: string;
}Admin Resource Definition
import { defineResource } from "@/lib/resource";
export const postResource = defineResource({
name: "Post",
slug: "posts",
endpoint: "/api/posts",
icon: "FileText",
label: { singular: "Post", plural: "Posts" },
table: {
columns: [
{ key: "id", label: "ID", sortable: true, width: "80px" },
{ key: "title", label: "Title", sortable: true, searchable: true },
{ key: "content", label: "Content", searchable: true },
{ key: "published", label: "Published", format: "boolean" },
{ key: "created_at", label: "Created", sortable: true, format: "relative" },
],
filters: [
{ key: "published", label: "Published", type: "boolean" },
],
defaultSort: { key: "created_at", direction: "desc" },
searchable: true,
pageSize: 20,
},
form: {
fields: [
{ key: "title", label: "Title", type: "text", required: true },
{ key: "content", label: "Content", type: "textarea" },
{ key: "published", label: "Published", type: "toggle" },
],
},
});Files Modified (10 Injection Points)
In addition to creating new files, the generator injects code into existing files to wire everything together. Each injection uses a marker comment that was placed during project scaffolding.
| # | Injection | File |
|---|---|---|
| 1 | Add model to AutoMigrate list | models/user.go |
| 2 | Add model to GORM Studio mount | routes/routes.go |
| 3 | Initialize handler struct | routes/routes.go |
| 4 | Register protected routes (list, get, create, update) | routes/routes.go |
| 5 | Register admin routes (delete) | routes/routes.go |
| 6 | Export Zod schemas from index | schemas/index.ts |
| 7 | Export TypeScript types from index | types/index.ts |
| 8 | Add API route constants | constants/index.ts |
| 9 | Import resource definition | resources/index.ts |
| 10 | Register resource in the registry array | resources/index.ts |
The Marker System
Grit uses special marker comments in scaffolded files to know where to inject new code. These markers are placed during project creation and should not be removed. The generator finds each marker and inserts new code on the line before it.
| Marker | File | Purpose |
|---|---|---|
| // grit:models | models/user.go | Add new model to AutoMigrate |
| /* grit:studio */ | routes/routes.go | Add model to GORM Studio (inline) |
| // grit:handlers | routes/routes.go | Initialize handler struct |
| // grit:routes:protected | routes/routes.go | Register authenticated API routes |
| // grit:routes:admin | routes/routes.go | Register admin-only routes |
| // grit:schemas | schemas/index.ts | Export Zod schemas |
| // grit:types | types/index.ts | Export TypeScript types |
| // grit:api-routes | constants/index.ts | Add API route constants |
| // grit:resources | resources/index.ts | Import resource definition |
| // grit:resource-list | resources/index.ts | Register in resources array |
Important: Never remove marker comments from your files. If a marker is missing, the generator will skip that injection and print a warning. You can safely add your own code above or below the markers.
How Injection Works
There are two injection modes used by the generator:
injectBefore (line-based)
Most markers use this mode. The generator finds the marker comment, then inserts the new code on the line immediately before it. This is used for model registration, route definitions, exports, and resource imports.
injectInline (same-line)
The GORM Studio marker uses this mode. The generator inserts code directly before the marker on the same line. This produces clean output like: &models.Post{}, /* grit:studio */
YAML Definition Format
For complex resources, use a YAML file to define the fields. This gives you more control over field properties like required, unique, and default values.
name: Invoice
fields:
- name: number
type: string
required: true # adds binding:"required" in Go, .min(1) in Zod
unique: true # adds gorm:"uniqueIndex"
- name: description
type: text # maps to gorm:"type:text", optional by default
- name: amount
type: float
- name: status
type: string
default: "pending" # adds gorm:"default:pending"
- name: quantity
type: uint # unsigned integer, z.number().int().nonnegative()
- name: due_date
type: date # maps to *time.Time, gorm:"type:date"
- name: paid
type: bool
default: "false" # adds gorm:"default:false"YAML Field Properties
| Property | Type | Default | Effect |
|---|---|---|---|
| name | string | (required) | Field name, auto-converted to PascalCase/snake_case |
| type | string | (required) | One of: string, text, richtext, int, uint, float, bool, datetime, date, slug, belongs_to, many_to_many, string_array |
| required | bool | true for string | Adds Go binding tag and Zod .min(1) validator |
| unique | bool | false | Adds gorm:"uniqueIndex" tag |
| default | string | (none) | Adds gorm:"default:value" tag |
Complete Field Type Mappings
Each Grit field type maps to specific types and behaviors across the entire stack. This table shows the full mapping from field type to Go, GORM, TypeScript, Zod, DataTable column format, and form field type.
| Grit Type | GORM Tag | Column | Form | Sortable | Searchable |
|---|---|---|---|---|---|
| string | size:255 | text | text | Yes | Yes |
| text | type:text | text | textarea | No | Yes |
| int | (none) | text | number | Yes | No |
| uint | (none) | text | number | Yes | No |
| float | (none) | text | number | Yes | No |
| bool | (none) | boolean | toggle | No | No |
| datetime | (none) | relative | datetime | Yes | No |
| date | type:date | relative | date | Yes | No |
| richtext | type:text | richtext | richtext | No | Yes |
| slug | size:255;uniqueIndex | text | (excluded) | Yes | Yes |
| belongs_to | index | text | relationship-select | No | No |
| many_to_many | (junction table) | (hidden) | multi-relationship-select | No | No |
| string_array | type:json | text | images | No | No |
Auto-Generated Search
The generator automatically builds search queries based on your field types. All string andtext fields are included in the search WHERE clause using PostgreSQL's ILIKE operator for case-insensitive matching.
// For --fields "title:string,content:text,published:bool"
// The generator produces:
query.Where("title ILIKE ? OR content ILIKE ?",
"%"+search+"%", "%"+search+"%")
// Notice: bool and numeric fields are NOT included in text search.
// Only string and text fields participate in ILIKE search.If a resource has no string or text fields, the generator falls back to searching by ID: id::text ILIKE ?.
Interactive Mode
When you use the -i flag, the CLI enters interactive mode where you define fields one at a time. Each field is entered as name:type. The prompt shows all valid types and provides real-time validation.
- ✓Invalid types are rejected immediately with a warning
- ✓Invalid field name formats are rejected
- ✓Each successfully added field gets a confirmation
- ✓Press Enter on an empty line to finish defining fields
- ✓At least one field is required