GORM basics

Models, tags, the convention surface.

7 mineasy

GORM is the ORM that ships with Grit. It speaks Postgres and SQLite out of the box; you write Go structs, it writes SQL. This lesson is the 7-minute crash course you need to read every Grit model.

A model is a Go struct + tags

apps/api/internal/models/customer.go
package models
import (
"time"
"github.com/google/uuid"
)
type Customer struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
Name string `gorm:"not null" json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

The tags that matter

  • gorm:"primaryKey" — this field is the PK
  • gorm:"type:uuid" — column type (use uuid for IDs; Grit defaults to UUID PKs)
  • gorm:"not null" — DB-level NOT NULL constraint
  • gorm:"uniqueIndex" — unique index on this column
  • gorm:"default:value" — column default
  • gorm:"-" — skip this field entirely (never write to DB)
  • json:"snake_name" — how it appears in the API response

UUID, not auto-increment

Grit uses UUIDs for primary keys, not uint auto-increment. Two reasons: (1) IDOR defence — attackers can't guess the next ID; (2) globally unique — you can generate them client-side and merge across DBs without conflict.

// Auto-generated in a BeforeCreate hook
func (c *Customer) BeforeCreate(tx *gorm.DB) error {
if c.ID == uuid.Nil {
c.ID = uuid.New()
}
return nil
}

You don't need to write this hook yourself if you use grit generate resource — it's already in the template.

CreatedAt + UpdatedAt — free

GORM auto-sets CreatedAt on insert and UpdatedAt on every save. No work on your part — just declare them with the right names and types.

Soft deletes — adding DeletedAt gorm.DeletedAt `gorm:"index"` turns on soft delete. Calling db.Delete(&customer) sets the column; queries automatically exclude soft-deleted rows. Useful for audit-able domains.

Reading + writing

// Create
c := &Customer{Email: "alex@example.com", Name: "Alex"}
db.Create(c)
// Read by ID
var c Customer
db.First(&c, "id = ?", id)
// Update one field
db.Model(&c).Update("name", "Alex Doe")
// Query with WHERE
var customers []Customer
db.Where("email LIKE ?", "%@example.com").Find(&customers)

Always pass arguments as the second+ parameter — never concatenate strings (covered as SQLi defence in the Concepts course).

Quick check

A teammate writes db.Raw("SELECT * FROM users WHERE email = '" + email + "'").Scan(&u). What's wrong?

Try it

Add a Customer model to your bench-api. Then write three lines of code in main.go (or a temporary debug handler) that create one and read it back.

db.AutoMigrate(&Customer{})
c := &Customer{Email: "alex@example.com", Name: "Alex"}
db.Create(c)
var found Customer
db.First(&found, "email = ?", "alex@example.com")
log.Println("found:", found.ID, found.Name)

Paste the log output in notes.md.

What's next

Single tables are easy. Real apps have relations — Customer has many Invoices; an Invoice belongs to a Customer. Next lesson: the three relation kinds and how Grit defines each.

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