Anatomy of grit generate — Contact end-to-end
Every token of the command, the full field-type table, the three ways to run it.
Time to dissect the command, then run it. By the end of this lesson you understand what every token in grit generate resource Contact --fields "…" means, you've seen the full list of field types and modifiers, and you have eight new files on disk for a Contact resource with name, email, and phone.
Anatomy of the command
Every Grit resource you ever generate has the same shape. Once you read this once it stops being magic:
grit generate resource Contact --fields "name:string,email:string:unique,phone:string:optional"└──┬───┘ └──┬───┘ └──┬───┘ └──┬──┘ └──┬───┘ └──────────────────────┬──────────────────────────────┘│ │ │ │ │ ││ │ │ │ │ └── Field spec — comma-separated list. Each│ │ │ │ │ field is "name:type" plus optional modifiers.│ │ │ │ ││ │ │ │ └── Flag. Tells the CLI you're passing fields inline.│ │ │ │ Alternatives: --from contact.yaml | -i (interactive prompts)│ │ │ ││ │ │ └── Resource name. PascalCase, singular. Grit pluralises for the URL ("contacts")│ │ │ and snake_cases for the file ("contact.go"). Don't write "contacts" or "contact_model".│ │ ││ │ └── Subcommand. "resource" is the full vertical slice (model + service + handler + routes +│ │ schema + type + hook + admin page). There's also "grit generate scaffold" / "grit generate ai"│ │ but resource is the workhorse you'll use every day.│ ││ └── Verb. "generate" writes new files. (Compare with "grit sync" — that one reads existing files.)│└── The CLI binary. Installed once via go install; lives on your PATH.
Each field spec inside the quotes has its own anatomy:
email : string : unique└─┬─┘ └──┬─┘ └─┬──┘│ │ ││ │ └── Modifier(s). Zero or more, colon-separated. Valid: required, optional, unique.│ │ (string fields are required by default — add :optional to make them nullable.)│ ││ └── Type. One of: string, text, richtext, int, uint, float, bool, datetime, date,│ slug, belongs_to, many_to_many, string_array, file, files.│└── Field name. camelCase or snake_case in input — Grit normalises to PascalCase in Goand snake_case in JSON ("Email" in the struct, "email" in the JSON body).
Run it
The simplest possible useful resource: a Contact with a name, email, and phone. Run this from the project root:
$grit generate resource Contact \$ --fields "name:string,email:string:unique,phone:string:optional"
You'll see:
Generating resource: Contact✓ apps/api/internal/models/contact.go✓ apps/api/internal/services/contact.go✓ apps/api/internal/handlers/contact.go✓ packages/shared/schemas/contact.ts✓ packages/shared/types/contact.ts✓ apps/web/hooks/use-contacts.ts✓ apps/admin/resources/contacts.ts✓ apps/admin/app/(dashboard)/resources/contacts/page.tsxInjecting into existing files...✓ Injected model into AutoMigrate✓ Injected model into GORM Studio✓ Injected handler initialization✓ Injected protected routes✓ Injected schema export✓ Injected type export✓ Injected API route constants✓ Injected resource import into registry✓ Injected resource into registry list✅ Resource Contact generated successfully!Next steps:1. cd apps/api && go build ./...2. Restart the API server3. The admin panel will show Contacts in the sidebar
grit migrate, the model exists in code but the contacts table doesn't. Hitting GET /api/contacts before migration returns a 500 with relation "contacts" does not exist. Always: generate, then migrate.Field types — the full list
Fifteen types cover almost everything. Pick the one that matches the meaning of the field, not the storage — the generator handles the storage mapping for you.
| Type | Go | TypeScript | Admin input | Use it for |
|---|---|---|---|---|
| string | string | string | text input | short single-line text (name, email, url, phone) |
| text | string | string | textarea | multi-line plain text (notes, description) |
| richtext | string | string | Word-style editor | formatted body content (blog post, article) |
| int | int | number | number input | whole numbers, can be negative |
| uint | uint | number | number input (≥ 0) | counts, stock quantities, page views |
| float | float64 | number | number input | money (auto-becomes decimal — see below), ratings, percentages |
| bool | bool | boolean | toggle | yes/no flags (is_active, featured, published) |
| date | *time.Time | string | null | date picker | birthdays, deadlines — date-only, no time component |
| datetime | *time.Time | string | null | datetime picker | timestamps with hours/minutes (scheduled_at, published_at) |
| slug | string | string | hidden (auto) | URL-friendly identifier, auto-generated from another field |
| belongs_to | string (UUID FK) | string | relationship dropdown | one-to-many parent (contact → group) |
| many_to_many | []string | string[] | multi-select dropdown | many-to-many (post → tags, user → roles) |
| string_array | JSONSlice[string] | string[] | multi-image uploader | photo gallery, screenshot list, or freeform tag array |
| file | *FileRef | FileRef | null | file dropzone | a single uploaded file with name + mime + size metadata |
| files | FileRefs | FileRef[] | files dropzone | mixed-type file gallery (pdf + doc + zip…) |
Several of these — slug, belongs_to, many_to_many, file, and files — have their own field-spec syntax (a third colon-separated part for the source field, related model, or accept list). Those are covered in the Field types deep dive, File fields + Excel I/O, and Relationships lessons later in this chapter.
Quick taste of the file syntax: hero:file:image means "single file, only images accepted"; attachments:files:[pdf,doc,image] means "multi-file gallery, only PDFs, Word docs, or images allowed". Valid accept aliases are: image, video, audio, pdf, doc, excel, csv, zip, archive, all. The list is both a UI filter and a runtime MIME check on the upload endpoint.
Modifiers — the full list
Only three. That's it.
required— column is NOT NULL, Zod requires the field on Create. Strings are required by default, so you usually only set this on non-string types you want to enforce.optional— column is nullable, Zod allows missing. Useful to flip a string off its default-required state (e.g.phone:string:optional).unique— adds a database unique index. Two contacts can't share an email if you mark it unique.
default=value? It exists, but only in the long-form YAML definition (next lesson). The inline --fields string is intentionally minimal — three modifiers, no quoted values, no escaping headaches. Reach for YAML once your fields outgrow one line.Smart heuristics — names that earn extra storage
Grit looks at field names, not just types, when picking column types. Three patterns trigger smart defaults:
| Name pattern | Becomes | Why |
|---|---|---|
| avatar, logo, banner, photo, thumbnail, image, *_url, … | VARCHAR(500) | Signed S3 URLs and UTM-tagged links blow past 255. |
| description, notes, content, body, summary, bio, message, … | TEXT | Long-form content shouldn't be VARCHAR — gets truncated and limits search. |
| price, amount, total, *_cost, *_fee, *_balance (on a float field) | DECIMAL(12,2) | Float arithmetic on money has rounding bugs (1.99 + 0.01 ≠ 2.00). Fixed precision avoids them. |
So price:float on a Product gets DECIMAL(12,2) automatically — no need to spell that out. Just name the field what you mean.
Three ways to call the generator
The same Contact resource, three different forms:
1. Inline (short form)
$grit generate resource Contact \$ --fields "name:string,email:string:unique,phone:string:optional"
Best for: quick resources, 1–5 fields, no defaults. Reads well in a commit message.
2. YAML file (long form)
name: Contactfields:- name: nametype: stringrequired: true- name: emailtype: stringrequired: trueunique: true- name: phonetype: stringrequired: false- name: statustype: stringdefault: active
$grit generate resource Contact --from contact.yaml
Best for: 5+ fields, fields that need default values, anything you'll re-generate or check into the repo.
3. Interactive (no flags)
$grit generate resource Contact -i
Best for: pairing with someone, exploring what's available, or when you haven't named all the fields yet. The CLI walks you through each field one prompt at a time.
Quick check
Try it
Generate the Contact resource on your machine:
$grit generate resource Contact \$ --fields "name:string,email:string:unique,phone:string:optional"$grit migrate# restart dev servers if they don't hot reload
Then hit GET /api/contacts (with your admin JWT) and paste the response — the empty paginated list — into notes.md.
What's next
Eight files showed up. Next lesson we tour each one — open every generated file for Contact, read its full content, and connect the layers mentally so you know exactly what to edit when the defaults aren't enough.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub