Project tour

cmd/server, internal/, the Services pattern.

8 mineasy

Let's walk every folder inside apps/api/. By the end you'll know what each file does and where to put new code without thinking.

The full tree

apps/api/
apps/api/
ā”œā”€ā”€ cmd/
│ └── server/
│ └── main.go ← entry point, wires services + starts Gin
ā”œā”€ā”€ internal/
│ ā”œā”€ā”€ config/ loads .env into a typed Config struct
│ ā”œā”€ā”€ database/ Postgres / SQLite connection + AutoMigrate
│ ā”œā”€ā”€ handlers/ HTTP handlers — thin
│ ā”œā”€ā”€ middleware/ auth, CORS, security headers, request ID
│ ā”œā”€ā”€ models/ GORM structs (User, Upload, …)
│ ā”œā”€ā”€ routes/
│ │ └── routes.go mounts every handler under /api/*
│ ā”œā”€ā”€ services/ business logic — thick
│ ā”œā”€ā”€ ai/ AI Gateway client
│ ā”œā”€ā”€ cache/ Redis cache
│ ā”œā”€ā”€ jobs/ Asynq queue + workers
│ ā”œā”€ā”€ mail/ Resend client + templates
│ ā”œā”€ā”€ storage/ S3 / MinIO client
│ └── totp/ TOTP 2FA (encrypted seed storage)
ā”œā”€ā”€ go.mod
ā”œā”€ā”€ .air.toml hot-reload config
└── Dockerfile

cmd/server/main.go — the entry point

This is where Gin starts. The full flow:

apps/api/cmd/server/main.go (abridged)
func main() {
cfg := config.Load()
db := database.MustConnect(cfg)
// Instantiate every service the handlers need
svc := &routes.Services{
Cache: cache.New(cfg),
Storage: storage.New(cfg),
Mailer: mail.New(cfg),
}
r := routes.Setup(db, cfg, svc) // builds the Gin router
r.Run(":" + cfg.Port)
}

Roughly 30 lines. main.go is intentionally tiny — its job is to wire dependencies, not to contain logic.

The Services pattern

Every external dependency (Redis, S3, Resend, AI Gateway) is wrapped in a tiny service. Those services compose into a single routes.Services struct that's passed to routes.Setup(). Handlers get the services they need by reference; nothing is global.

Why this matters: tests can pass a mock Services struct and exercise handlers without booting Redis / S3 / Resend. The handler doesn't care if the storage backend is real S3 or a fake in-memory one.

internal/ — recap from Concepts

You met this folder in Grit Concepts ch.3. Quick refresher of the hot path:

request → middleware → routes.go picks handler → handler calls service
→ service calls model
→ response shaped by handler

The new folders for batteries

  • jobs/ — Asynq integration. Enqueue from anywhere; the worker (separate process) consumes them.
  • mail/ — Resend client + HTML templates. In dev, all mail goes to Mailhog.
  • storage/ — S3-compatible (R2, MinIO, B2). One interface, swap the backend via env.
  • ai/ — AI Gateway client. Stream from Claude, OpenAI, and ~98 other models.
  • totp/ — 2FA: TOTP seeds encrypted at rest via AES-GCM.

Quick check

You want to add a Stripe webhook handler. Which two files do you touch first?

Try it

Open apps/api/internal/routes/routes.go. Count how many route groups are registered (look for r.Group(...) or api.Group(...)). For each, write one line in notes.md describing what it serves.

What's next

You know the layout. Last lesson of this chapter — start everything up, make a request, prove the API works.

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