grit sync — multi-surface
One Go model, three TS frontends.
With four frontends, type drift is the silent killer. Web thinks price is a number; mobile thinks it's a string; desktop forgot to update after the API renamed it. Cue the production bug. grit sync exists to kill that whole category.
One Go model, four frontends
grit sync flow
The Go model is the source of truth
package modelsimport "time"type Product struct {ID uint `gorm:"primaryKey" json:"id"`Name string `gorm:"not null" json:"name"`Slug string `gorm:"uniqueIndex" json:"slug"`PriceCents int `gorm:"not null" json:"price_cents"`Currency string `gorm:"default:USD" json:"currency"`InStock bool `json:"in_stock"`Tags string `gorm:"type:text" json:"tags"` // CSVCreatedAt time.Time `json:"created_at"`UpdatedAt time.Time `json:"updated_at"`}
Run grit sync
grit sync# scans apps/api/internal/models/*.go# writes packages/shared/types/*.ts# writes packages/shared/zod/*.ts
What gets generated
// Generated by grit sync. Do not edit.export interface Product {id: numbername: stringslug: stringprice_cents: numbercurrency: stringin_stock: booleantags: stringcreated_at: stringupdated_at: string}export interface CreateProductInput {name: stringslug: stringprice_cents: numbercurrency?: stringin_stock?: booleantags?: string}
Notice: CreateProductInput drops the server-managed fields (ID, timestamps) and makes defaulted-fields optional. That's what you POST to /api/products.
The naming rule — snake vs. camel
Go convention is PriceCents; JSON convention is price_cents. The TS file mirrors the JSON tag, NOT the Go field name, because that's what comes off the wire.
If you prefer camelCase on the frontend, change the JSON tag in Go: json:"priceCents". The frontend type will update next sync. Pick one and stay consistent.
Add the field once — all frontends gain it
Suppose marketing asks for a Featured badge. Steps:
- Add
IsFeatured boolto the Go struct. grit sync.- Web, admin, mobile, desktop — all four IDEs now show the field.
- Run a DB migration (grit generates one too) so the column exists.
Five seconds of typing, zero copies, zero drift.
What sync does NOT do
- Doesn't generate API client code. The shared package gives you the types; each surface still calls
fetch(web/admin/desktop) or the mobile API wrapper. Types make the calls safe, not automatic. - Doesn't touch your existing UI. Add a field, the type changes; whether the UI shows it is on you. TS will flag where you destructure the type but ignore the new field as a soft warning if you enable it.
- Doesn't roll back a deleted Go field. Remove a field in Go, sync, and the TS shape becomes narrower. Any code referencing the old field now type-errors — which is the point.
Quick check
Try it
Feel the four-surface wave:
- Add
IsFeatured bool `gorm:"default:false" json:"is_featured"`to the Product Go model. - Run
grit syncfrom the repo root. - Open the generated
packages/shared/types/product.ts— confirmis_featuredis there. - Run
pnpm type-checkat the root. Should pass (we're adding a field, not breaking existing code). - Use
product.is_featuredin ONE component in each of web, admin, mobile, desktop — even just rendering "★" if true. Confirm autocomplete in all four IDEs.
What's next
Next lesson — Shared Zod schemas. Types tell you what shape data has; Zod validates that the runtime data actually matches that shape. Both sides of the input boundary.
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