Core Concepts

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:

1

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.

2

Parse definition

Reads the resource definition from --fields, --from (YAML), or -i (interactive). Validates all field names and types.

3

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.

4

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.

FileContains
apps/api/internal/models/post.goGORM struct with all fields, tags, timestamps, soft delete
apps/api/internal/services/post.goBusiness logic: List (paginated), GetByID, Create, Update, Delete
apps/api/internal/handlers/post.goGin handlers: List, GetByID, Create, Update, Delete with validation
packages/shared/schemas/post.tsCreatePostSchema, UpdatePostSchema with Zod validators
packages/shared/types/post.tsTypeScript interface with all fields, id, created_at, updated_at
apps/web/hooks/use-posts.tsReact Query hooks: usePosts, useGetPost, useCreatePost, useUpdatePost, useDeletePost
apps/admin/resources/posts.tsResource definition: columns, form fields, filters, dashboard widget
apps/admin/app/resources/posts/page.tsxAdmin 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

apps/api/internal/models/post.go
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

packages/shared/schemas/post.ts
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

packages/shared/types/post.ts
export interface Post {
  id: number;
  title: string;
  content: string;
  published: boolean;
  created_at: string;
  updated_at: string;
}

Admin Resource Definition

apps/admin/resources/posts.ts
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.

#InjectionFile
1Add model to AutoMigrate listmodels/user.go
2Add model to GORM Studio mountroutes/routes.go
3Initialize handler structroutes/routes.go
4Register protected routes (list, get, create, update)routes/routes.go
5Register admin routes (delete)routes/routes.go
6Export Zod schemas from indexschemas/index.ts
7Export TypeScript types from indextypes/index.ts
8Add API route constantsconstants/index.ts
9Import resource definitionresources/index.ts
10Register resource in the registry arrayresources/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.

MarkerFilePurpose
// grit:modelsmodels/user.goAdd new model to AutoMigrate
/* grit:studio */routes/routes.goAdd model to GORM Studio (inline)
// grit:handlersroutes/routes.goInitialize handler struct
// grit:routes:protectedroutes/routes.goRegister authenticated API routes
// grit:routes:adminroutes/routes.goRegister admin-only routes
// grit:schemasschemas/index.tsExport Zod schemas
// grit:typestypes/index.tsExport TypeScript types
// grit:api-routesconstants/index.tsAdd API route constants
// grit:resourcesresources/index.tsImport resource definition
// grit:resource-listresources/index.tsRegister 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.

invoice.yaml
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

PropertyTypeDefaultEffect
namestring(required)Field name, auto-converted to PascalCase/snake_case
typestring(required)One of: string, text, richtext, int, uint, float, bool, datetime, date, slug, belongs_to, many_to_many, string_array
requiredbooltrue for stringAdds Go binding tag and Zod .min(1) validator
uniqueboolfalseAdds gorm:"uniqueIndex" tag
defaultstring(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 TypeGORM TagColumnFormSortableSearchable
stringsize:255texttextYesYes
texttype:texttexttextareaNoYes
int(none)textnumberYesNo
uint(none)textnumberYesNo
float(none)textnumberYesNo
bool(none)booleantoggleNoNo
datetime(none)relativedatetimeYesNo
datetype:daterelativedateYesNo
richtexttype:textrichtextrichtextNoYes
slugsize:255;uniqueIndextext(excluded)YesYes
belongs_toindextextrelationship-selectNoNo
many_to_many(junction table)(hidden)multi-relationship-selectNoNo
string_arraytype:jsontextimagesNoNo

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.

generated search query for Post
// 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