Code Generator Mastery
In this course, you will master the grit generate resource command — the most powerful tool in the Grit CLI. You will learn how to generate full-stack CRUD features from a single command, understand every field type, use modifiers and relationships, and keep your Go and TypeScript types perfectly in sync.
What is Code Generation?
Every feature in a web app needs the same set of files: a database model to store data, a service for business logic, a handler for HTTP endpoints, validation schemas, TypeScript types, and React pages for the admin panel.
Instead of manually creating all of these files for every feature — which is tedious and error-prone — the Grit code generator creates all of them from one command. You describe your data (a "Book" with a title, author, and page count), and Grit writes every file, wires up every route, and registers everything in the admin panel automatically.
Your First Resource
Let's generate a Book resource. Open your terminal in the project root and run:
grit generate resource Book --fields "title:string,author:string,pages:int,published:bool"Let's break down each part of this command:
- •
grit generate resource— the command that triggers the code generator - •
Book— the resource name, always in PascalCase and singular (Book, not Books) - •
--fields— inline field definitions, a comma-separated list - •
title:string— a field definition: field name : field type
Generated Files
That single command creates all of these files:
apps/api/internal/models/book.go # Go model (database table)
apps/api/internal/services/book_service.go # Business logic (CRUD operations)
apps/api/internal/handlers/book_handler.go # HTTP endpoints (REST API)
packages/shared/schemas/book.ts # Zod validation schema
packages/shared/types/book.ts # TypeScript type definitions
apps/admin/app/(dashboard)/books/page.tsx # Admin page with DataTableIt also injects code into existing files:
- •
routes.go— registers all Book API routes (/api/books,/api/books/:id) - •
database.go— addsBookto the auto-migration list - •
main.go— initializes the Book service and handler
Challenge: Generate Your First Resource
Run the command above to generate a Book resource. After it completes, open your project in your editor and verify that each of the 6 files listed above was created. Check routes.go — can you find the new Book routes?
Understanding the Generated Go Model
Open apps/api/internal/models/book.go. You will see something like this:
package models
import "gorm.io/gorm"
type Book struct {
gorm.Model
Title string `gorm:"not null" json:"title"`
Author string `gorm:"not null" json:"author"`
Pages int `gorm:"not null" json:"pages"`
Published bool `gorm:"default:false" json:"published"`
}Explained
- •
gorm.Modelgives you four fields for free:ID(primary key),CreatedAt,UpdatedAt, andDeletedAt(for soft deletes) - •
gorm:"not null"means the database requires this field — it cannot be empty - •
json:"title"controls how the field appears in API responses (lowercase, matching JavaScript conventions) - • The struct name
Bookbecomes the database tablebooks(GORM automatically pluralizes and lowercases it) - •
gorm:"default:false"on thePublishedfield means new books are unpublished by default
Book groups Title, Author, Pages, and Published into one type.gorm:"not null" json:"title"). They are metadata that tells libraries how to handle the field. The gorm tag tells GORM how to create the database column. The json tag tells the JSON encoder what name to use in API responses.Challenge: Explore gorm.Model
Open the generated book.go model. Find the gorm.Model line. What 4 fields does it give you for free? (Hint: check the GORM documentation.)
Challenge: Understand GORM Tags
Look at the GORM tags on the model fields. What would happen if you removed not null from the Title field? Try it — restart the API and create a book without a title. Does the database accept it?
Field Types
Grit supports 13 field types. Each type maps to a Go type, a TypeScript type, and a database column type. Here is the complete reference:
| Type | Go | TypeScript | Database | Use for |
|---|---|---|---|---|
string | string | string | varchar | Short text (names, titles) |
text | string | string | text | Long text (descriptions, bios) |
richtext | string | string | text | HTML content (blog posts) — Tiptap editor in admin |
int | int | number | integer | Whole numbers (age, count) |
uint | uint | number | unsigned integer | Positive-only numbers (IDs) |
float | float64 | number | double precision | Decimal numbers (price, rating) |
bool | bool | boolean | boolean | True/false (published, active) |
datetime | time.Time | string (ISO 8601) | timestamp | Date and time (event_date) |
date | time.Time | string (ISO 8601) | date | Date only (birthday, due_date) |
slug | string | string | varchar (unique index) | URL-friendly string (auto-generated) |
belongs_to | uint | number | integer (FK) | Foreign key relationship |
many_to_many | []Model | Model[] | junction table | Many-to-many relationship |
string_array | pq.StringArray | string[] | jsonb | Array of strings (tags, categories) |
string, text, int, float, and bool. You will use belongs_to and many_to_many once you start building relationships between resources.Challenge: Pick the Right Type
Which field type would you use for each of these?
- 1. A product price (e.g., $29.99)
- 2. A blog post body with formatting
- 3. An article's publish date
- 4. A user's age
(Answers: float, richtext, date or datetime, int)
Generate with Different Fields
Let's see progressively more complex examples to build your intuition.
Example 1: Simple Blog Post
grit generate resource Post --fields "title:string,content:richtext,published:bool"This generates a Post with a short title, a rich text editor for the content (with bold, italic, headings, links, etc.), and a boolean toggle for published/draft status.
Example 2: Product with Price
grit generate resource Product --fields "name:string,description:text,price:float,stock:int,active:bool"Notice how description uses text (plain long text) instead of richtext — product descriptions usually don't need a rich text editor. The price uses float for decimal values, and stock uses int for whole numbers.
Example 3: Event with Dates
grit generate resource Event --fields "name:string,description:text,start_date:datetime,end_date:datetime,capacity:int"Events use datetime for start and end times. The admin form will show a date-time picker for these fields. The capacity field uses int for the maximum number of attendees.
Challenge: Generate a Post Resource
Generate the Post resource from Example 1. Open the admin panel at http://localhost:3001 and create a blog post. Does the richtext field show a rich text editor with formatting options?
Challenge: Generate a Product Resource
Generate the Product resource from Example 2. Create 3 products in the admin panel with different prices (e.g., $9.99, $24.50, $149.00). Does the DataTable show all three products with their prices?
Field Modifiers
Modifiers let you customize how a field behaves. You add them after the field type, separated by a colon.
:unique — No Duplicates
Adding :unique creates a unique index on the database column, so no two records can have the same value for that field.
grit generate resource Category --fields "name:string:unique,description:text:optional"With name:string:unique, if you try to create two categories both named "Technology", the database will reject the second one with a unique constraint error.
:optional — Nullable Fields
By default, all fields are required (NOT NULL in the database). Adding :optional allows the field to be empty (NULL).
In the example above, description:text:optional means a category can be created without a description. The name field is still required — every category must have a name.
email:string:unique:optional means the field is nullable, but if a value IS provided, it must be unique across all records.Challenge: Test the Unique Modifier
Generate the Category resource with name:string:unique. Create a category named "Technology". Now try to create another category with the exact same name. What error message do you get?
Challenge: Test the Optional Modifier
Using the same Category resource (which has description:text:optional), create a category WITHOUT filling in the description field. Does it save successfully? Now create one WITH a description. Both should work.
Slug Fields
A slug is a URL-friendly version of a string. The title "My First Blog Post" becomes the slug my-first-blog-post. Slugs are used in URLs so visitors see /articles/my-first-blog-post instead of /articles/42.
The format is slug:slug:source_field — the third part tells Grit which field to generate the slug from:
grit generate resource Article --fields "title:string,slug:slug:title,content:richtext"Here, slug:slug:title means: create a field called "slug", of type "slug", derived from the "title" field. When you create an Article with the title "Hello World", the slug is automatically set to hello-world.
If two articles have the same title, Grit appends a random suffix to keep the slug unique (e.g., hello-world-a3f2). Slugs always have a unique index in the database.
Challenge: Test Slug Generation
Generate the Article resource with a slug field. Create an article with the title "Hello World". What slug was auto-generated? Now create another article with the exact same title — is the slug different? Check both slugs in the admin panel.
Relationships: belongs_to
Real apps have related data. A blog post belongs to a category. An order belongs to a customer. The belongs_to type creates a foreign key relationship between two resources.
category_id on the posts table points to id on the categories table. This is how databases link related records together.The format is field_name:belongs_to:ParentModel:
# First, generate the parent resource
grit generate resource Category --fields "name:string:unique"
# Then generate the child with a belongs_to relationship
grit generate resource Post --fields "title:string,content:richtext,category_id:belongs_to:Category"This creates:
- • A
CategoryID uintfield on the Post model - • A foreign key index in the database
- • A dropdown select in the admin form to pick a category
- • The API automatically returns the related category data when fetching posts
belongs_to.Challenge: Build a belongs_to Relationship
Generate Category (with name:string), then Post with category_id:belongs_to:Category. In the admin panel, create 2 categories ("Tech" and "Design"), then create a post and select a category from the dropdown. Open GORM Studio to verify the category_id column has the correct value.
Relationships: many_to_many
Sometimes, a single belongs_to is not enough. A blog post can have many tags, and a tag can belong to many posts. This is a many-to-many relationship.
post_tags table with post_id and tag_id columns. Each row represents one connection between a post and a tag.The format is field_name:many_to_many:OtherModel:
# Generate the related resource first
grit generate resource Tag --fields "name:string:unique"
# Then generate with many_to_many
grit generate resource Post --fields "title:string,content:richtext,tag_ids:many_to_many:Tag"In the admin form, the tag_ids field renders as a multi-select — you can pick multiple tags for a single post. Behind the scenes, GORM manages the junction table automatically.
belongs_to and many_to_many on the same resource. For example, a Post can belong to one Category AND have many Tags: category_id:belongs_to:Category,tag_ids:many_to_many:TagChallenge: Build a many_to_many Relationship
Generate Tag (with name:string:unique), then Post with tag_ids:many_to_many:Tag. Create 3 tags ("Go", "React", "Grit"). Create a post and select multiple tags. Open GORM Studio and look for the post_tags junction table — do you see the connections?
Interactive Mode
Typing all fields on one long command line can be tedious, especially for resources with many fields. Grit offers two alternatives.
Interactive Prompts (-i)
Use the -i flag for a step-by-step interactive experience:
grit generate resource Product -iGrit will prompt you to add fields one at a time, asking for the field name, type, and any modifiers. Type "done" when you have added all your fields.
YAML Definition (--from)
For complex resources, you can define fields in a YAML file:
name: Product
fields:
- name: string
- description: text:optional
- price: float
- active: boolgrit generate resource Product --from product.yamlYAML files are great for resources you plan to regenerate or share with your team.
Challenge: Try Interactive Mode
Generate a Contact resource using interactive mode (-i). Add these fields one by one: name (string), email (string:unique), phone (string:optional), message (text). Verify all files are created correctly.
Removing a Resource
Made a mistake? Want to start over? The grit remove resource command cleanly removes everything:
grit remove resource BookThis deletes all generated files (model, service, handler, schemas, types, admin page) AND reverses all code injections (imports, route registrations, database migrations, admin entries).
Your project returns to exactly how it was before you ran grit generate resource Book.
Challenge: Remove and Verify
Run grit remove resource Book. Verify the model file apps/api/internal/models/book.go is gone. Check routes.go — are the Book routes removed? Check database.go — is Book removed from the migration list?
Type Sync
Sometimes you need to manually edit a Go model — add a field, change a type, tweak a tag. When you do, the TypeScript types and Zod schemas become out of date. That's where grit sync comes in.
grit syncThis command reads all your Go models, parses their struct fields and tags, and regenerates the matching TypeScript interfaces and Zod validation schemas. Your frontend types stay perfectly in sync with your backend models.
grit sync anytime you manually edit a Go model. It's fast and safe — it only updates the generated type files, never your custom code.Challenge: Test Type Sync
Open a generated Go model and manually add a new field (e.g., Rating float64 `gorm:"default:0" json:"rating"`). Run grit sync. Check the corresponding TypeScript type file — did a rating: number field appear? Check the Zod schema — was a rating validator added?
Code Markers
You might wonder: how does Grit know where to inject code into existing files? The answer is code markers — special comments placed in your scaffolded files.
// ... existing routes ...
bookGroup := api.Group("/books")
bookGroup.GET("", handler.BookHandler.GetAll)
bookGroup.POST("", handler.BookHandler.Create)
// grit:routesThe markers look like // grit:routes, // grit:models, and // grit:imports. When you generate a resource, Grit finds these markers and inserts code right before them. When you remove a resource, Grit deletes those injected lines.
The markers are invisible to your app (they're just comments), but they are essential for the code generator to work correctly.
// grit:routes marker, Grit won't know where to add routes for new resources. If you move them, code will be injected in the wrong place.Summary
You now have a complete understanding of Grit's code generator. Here's what you learned:
- Code generation basics — one command creates model, service, handler, schemas, types, and admin page
- Go model structure — gorm.Model, struct tags, field types, and how they map to database columns
- All 13 field types — string, text, richtext, int, uint, float, bool, datetime, date, slug, belongs_to, many_to_many, string_array
- Field modifiers — :unique for no duplicates, :optional for nullable fields
- Slug fields — auto-generated URL-friendly strings from a source field
- belongs_to relationships — foreign keys with dropdown selects in the admin
- many_to_many relationships — junction tables with multi-select in the admin
- Interactive mode and YAML — alternative ways to define fields
- Removing resources — clean removal of all generated files and injections
- Type sync — keeping Go and TypeScript types in sync with grit sync
- Code markers — how Grit knows where to inject and remove code
Challenge: Final Challenge: Build a Bookstore
Put everything together. Build a complete bookstore by generating these resources in order:
- Author —
name:string,bio:text:optional - Genre —
name:string:unique - Book —
title:string,slug:slug:title,isbn:string:unique,price:float,author_id:belongs_to:Author,genre_ids:many_to_many:Genre,synopsis:text:optional,published:bool
Then:
- • Create 3 authors, 5 genres, and 10 books in the admin panel
- • Assign each book to an author and multiple genres
- • Run
grit routesto see all generated endpoints - • Open the API docs and test the
GET /api/booksendpoint
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.