# Architecture This document explains how the pieces of `go-simple-api` fit together, and *why* they're structured this way - useful both as a reference and as a guide if you extend the project. ## High-level request flow Every incoming HTTP request passes through the same pipeline, built in `internal/router/router.go`: ``` request │ ▼ chimw.RequestID -- tags the request with a unique ID │ ▼ middleware.RequestLogger -- records start time, wraps the response writer │ ▼ chimw.Recoverer -- catches panics, converts them to a 500 │ ▼ chimw.Timeout(60s) -- cancels the request context if it runs too long │ ▼ cors.Handler -- validates cross-origin requests (browser only) │ ▼ httprate.LimitByIP(100/min) -- global rate limit │ ▼ sessions.LoadAndSave -- loads session data from Redis into context │ ▼ [ per-route middleware, e.g. httprate strict limit, or requireAuth ] │ ▼ handler -- e.g. handlers.Login, handlers.Me │ ▼ response written │ ▼ (back through the stack) middleware.RequestLogger logs the final status/duration ``` Each middleware is a function shaped `func(http.Handler) http.Handler`: it wraps the *next* thing in the chain, does something before calling `next.ServeHTTP(w, r)`, and optionally does something after. This is why ordering matters - `RequestLogger` wraps everything registered after it, so it can measure the full duration including all of those inner layers. ## Dependency construction (`cmd/api/main.go`) `main.go` is intentionally the only place that constructs the "big" shared resources - the logger, the database pool, the session manager - and it constructs each of them exactly once, then passes them down as explicit function arguments (`router.New(logger, db, sessions, cfg)`). This is a form of **dependency injection**: nothing deep in the call stack reaches for a global variable to get a database connection or a logger. Every package that needs one receives it explicitly, either as a constructor argument (`NewUserRepository(db)`) or a struct field (`AuthHandler.userRepo`). The benefit: you can trace exactly what any given piece of code depends on just by reading its constructor signature, and (if you add tests later) you can substitute a fake/mock dependency without any global state to fight with. ## Package responsibilities | Package | Responsibility | Should NOT contain | |---|---|---| | `config` | Read env vars into a typed struct | Any logic beyond defaults/parsing | | `logging` | Build the shared `*slog.Logger` | Per-request logging logic (that's `middleware`) | | `database` | Open the MySQL pool, run migrations | Table-specific queries (that's `models`) | | `models` | Domain structs + repositories (all SQL) | HTTP concerns (status codes, JSON) | | `session` | Build the `*scs.SessionManager` | Route-specific session key names beyond `session.UserIDKey` | | `oauth` | Build provider `*oauth2.Config` values | Handling the actual HTTP callback (that's `handlers`) | | `handlers` | Parse requests, call into models/session, write responses | Raw SQL, direct Redis calls | | `middleware` | Cross-cutting HTTP behavior (logging, auth) | Business logic specific to one route | | `router` | Wire dependencies + register routes | Any actual request handling logic | If you're ever unsure where a new piece of code belongs, this table is the first place to check. ## The repository pattern (`internal/models`) `UserRepository` is the *only* place in the entire codebase that writes SQL for the `users` table. Handlers call methods like `FindByEmail` or `Create` - they never see a raw `*sql.DB` or write a query themselves. Why this matters in practice: - If you swap MySQL for PostgreSQL later, you change `user_repository.go` only - no handler code changes. - SQL injection risk is contained to one file, and that file consistently uses parameterized queries (`?` placeholders), never string concatenation. - Errors are translated at the boundary: `sql.ErrNoRows` (a database/sql-specific sentinel) becomes `models.ErrUserNotFound` (an application-specific sentinel), so callers reason about "not found" as a concept, not a SQL implementation detail. ## Sessions: how "server-side" actually works 1. `session.New(cfg)` builds a `*scs.SessionManager` whose `.Store` is Redis-backed (`internal/session/session.go`). 2. `sessions.LoadAndSave` (applied as middleware in `router.go`) runs on every request: it reads the `session_id` cookie, loads the corresponding session data from Redis into the request's `context.Context`, lets the handler run, then - after the handler returns - saves any changes back to Redis and sets/refreshes the cookie on the response. 3. Handlers never touch cookies or Redis directly. They call `sessions.Put(ctx, key, value)` / `sessions.GetInt(ctx, key)` / `sessions.Destroy(ctx)`, and the manager handles the rest via the context it already loaded in step 2. 4. Only the user's numeric ID is stored in the session (`session.UserIDKey`) - never the full user object. This keeps the session tiny and guarantees `/me` and `middleware.RequireAuth` always see fresh data from the database, never a stale cached copy. ## Authentication middleware and `context.Context` `middleware.RequireAuth` (`internal/middleware/require_auth.go`) is the single place that decides "is this request authenticated?" It: 1. Reads `session.UserIDKey` from the session. 2. Looks the user up in the database via `UserRepository.FindByID`. 3. On success, stores the `*models.User` in the request's `context.Context` under a private key, and calls `next.ServeHTTP` with the *new* request (contexts and requests are immutable - `context.WithValue` and `r.WithContext` both return new values rather than mutating in place). 4. On any failure, responds 401 immediately and `next.ServeHTTP` is never called - the wrapped handler doesn't run at all. Handlers that need the current user call `middleware.CurrentUser(r)`, which does the type assertion back out of the context. They never see or touch the context key itself, which is intentionally unexported. To protect a new route, add it inside the `r.Group(func(r chi.Router) { r.Use(requireAuth); ... })` block in `router.go`. ## Google OAuth2 flow in detail ``` Browser This API Google │ │ │ │ GET /auth/google/login │ │ ├───────────────────────────►│ │ │ │ generate random `state`, │ │ │ store it in session │ │ 302 redirect to Google │ │ │◄───────────────────────────┤ │ │ │ │ user logs in / approves, entirely on Google's own site │ │────────────────────────────────────────────────────────────► │ │ │ 302 redirect back with ?state=...&code=... │ │◄──────────────────────────────────────────────────────────── │ │ │ │ GET /auth/google/callback │ │ ├───────────────────────────►│ │ │ │ verify state matches │ │ │ POST code -> exchange for token │ │ ├──────────────────────────────►│ │ │◄──────────────────────────────┤ │ │ GET userinfo with token │ │ ├──────────────────────────────►│ │ │◄──────────────────────────────┤ │ │ find-or-create local user, │ │ │ renew session token, │ │ │ store user ID in session │ │ 200 OK { id, email } │ │ │◄───────────────────────────┤ │ ``` The `state` parameter exists purely as CSRF protection for the login flow itself - without it, an attacker could craft a callback URL using their own Google account and trick a victim's browser into using it. ## Docker networking Inside `docker-compose.yml`, each service's *name* becomes its hostname on the internal Docker network Compose creates automatically. That's why the `app` service is configured with `DB_HOST: mysql` and `REDIS_ADDR: redis:6379` instead of `127.0.0.1` - Compose's built-in DNS resolves `mysql` and `redis` to the correct container IPs. This is also exactly why `internal/config` reads these values from environment variables instead of hardcoding them: the same compiled binary works unchanged whether it's running on your laptop directly or inside this Compose network - only the environment variables differ. ## Logging shape (for Grafana Loki / Alloy) Every log line the app writes is a single JSON object to stdout, e.g.: ```json {"time":"2026-07-15T10:00:05Z","level":"INFO","msg":"http_request","request_id":"...","method":"GET","path":"/health","status":200,"bytes":16,"duration_ms":123000,"remote_addr":"127.0.0.1:54321"} ``` This shape is deliberately Alloy/Loki-friendly: consistent JSON keys mean Alloy can scrape container stdout and ship structured log lines without custom parsing rules, and you can filter/query in Loki on fields like `status`, `path`, or `request_id` directly.