Project tour
cmd/server, internal/, the Services pattern.
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/āāā 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:
func main() {cfg := config.Load()db := database.MustConnect(cfg)// Instantiate every service the handlers needsvc := &routes.Services{Cache: cache.New(cfg),Storage: storage.New(cfg),Mailer: mail.New(cfg),}r := routes.Setup(db, cfg, svc) // builds the Gin routerr.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.
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
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