Anatomy of grit generate — Contact end-to-end

Every token of the command, the full field-type table, the three ways to run it.

10 mineasy

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 Go
and 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:

Terminal
$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.tsx
Injecting 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 server
3. The admin panel will show Contacts in the sidebar
The generator writes Go code, not database schema. Until you run 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.

TypeGoTypeScriptAdmin inputUse it for
stringstringstringtext inputshort single-line text (name, email, url, phone)
textstringstringtextareamulti-line plain text (notes, description)
richtextstringstringWord-style editorformatted body content (blog post, article)
intintnumbernumber inputwhole numbers, can be negative
uintuintnumbernumber input (≥ 0)counts, stock quantities, page views
floatfloat64numbernumber inputmoney (auto-becomes decimal — see below), ratings, percentages
boolboolbooleantoggleyes/no flags (is_active, featured, published)
date*time.Timestring | nulldate pickerbirthdays, deadlines — date-only, no time component
datetime*time.Timestring | nulldatetime pickertimestamps with hours/minutes (scheduled_at, published_at)
slugstringstringhidden (auto)URL-friendly identifier, auto-generated from another field
belongs_tostring (UUID FK)stringrelationship dropdownone-to-many parent (contact → group)
many_to_many[]stringstring[]multi-select dropdownmany-to-many (post → tags, user → roles)
string_arrayJSONSlice[string]string[]multi-image uploaderphoto gallery, screenshot list, or freeform tag array
file*FileRefFileRef | nullfile dropzonea single uploaded file with name + mime + size metadata
filesFileRefsFileRef[]files dropzonemixed-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.
Looking for 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 patternBecomesWhy
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, …TEXTLong-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)

Terminal
$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)

contact.yaml
name: Contact
fields:
- name: name
type: string
required: true
- name: email
type: string
required: true
unique: true
- name: phone
type: string
required: false
- name: status
type: string
default: active
Terminal
$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)

Terminal
$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

You generated a Contact resource but `GET /api/contacts` returns 500. What did you forget?

Try it

Generate the Contact resource on your machine:

Terminal
$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