5.1 KiB
5.1 KiB
The course this project was built from
This project was built incrementally across 10 lessons, each adding one concept on top of the last. This file is a map from "concept" to "where it lives in the code" - useful if you want to revisit how/why something was built the way it was.
| # | Lesson | New concepts | Where it lives |
|---|---|---|---|
| 1 | Project skeleton & chi routing | Standard Go project layout, chi.Mux, middleware basics, graceful shutdown via http.Server + srv.Shutdown() |
cmd/api/main.go, internal/router/router.go, internal/handlers/health.go |
| 2 | Structured JSON logging | log/slog, slog.NewJSONHandler, log levels, the three-layer middleware-factory pattern |
internal/logging/logger.go, internal/middleware/request_logger.go |
| 3 | Config & MySQL connection | database/sql, connection pooling (SetMaxOpenConns etc.), DSNs, context.WithTimeout for a hard deadline on the initial ping |
internal/config/config.go, internal/database/mysql.go |
| 4 | User model & repository pattern | Pointers (*/&) in depth, pointer receivers, the repository pattern, sentinel errors + errors.Is |
internal/models/user.go, internal/models/user_repository.go |
| 5 | Password login | bcrypt hashing/salting, decoding JSON request bodies, struct tags, generic error messages to avoid user enumeration |
internal/handlers/auth.go (Register/Login), internal/handlers/respond.go |
| 6 | Server-side sessions (scs + Redis) | scs.SessionManager, swapping storage backends via .Store, cookie flags (HttpOnly, SameSite), RenewToken to prevent session fixation |
internal/session/session.go, internal/session/keys.go, Login/Logout/Me in internal/handlers/auth.go |
| 7 | Login with Google (OAuth2) | Authorization Code flow, oauth2.Config, CSRF state parameter, account linking by email |
internal/oauth/google.go, internal/handlers/oauth_google.go |
| 8 | Auth middleware & route protection | context.Context (WithValue/Value), private context-key types, type assertions, chi route groups |
internal/middleware/require_auth.go, r.Group(...) in internal/router/router.go |
| 9 | Rate limiting & security hardening | httprate.LimitByIP, CORS (go-chi/cors), environment-aware Secure cookie flag |
internal/router/router.go, internal/session/session.go, internal/config/config.go |
| 10 | Docker & wrap-up | Multi-stage Docker builds, docker-compose, service-name-as-hostname networking, named volumes |
Dockerfile, docker-compose.yml |
Core Go ideas that recur throughout the codebase
These aren't tied to a single lesson - once introduced, they show up repeatedly, and are worth having solid:
- Pointers (
*/&) - sharing one instance of something stateful (*sql.DB,*scs.SessionManager,*slog.Logger) across the whole app instead of copying it; writing a result back into a caller's variable (rows.Scan(&x),u.ID = int(id)insideCreate(ctx, u *User)). - Interfaces satisfied implicitly -
*chi.Muxsatisfieshttp.Handlerjust by having aServeHTTPmethod; there's noimplementskeyword in Go. - Closures / the three-layer middleware pattern - seen in both
RequestLogger(logger)andRequireAuth(sessions, userRepo, logger): an outer function captures dependencies, returns afunc(http.Handler) http.Handler, which itself returns the actual per-request handler - three layers, each running at a different time. context.Context- carrying request-scoped values (the current user, a request ID) and deadlines (timeouts) through a call chain without adding extra parameters to every function signature.- Error wrapping and sentinel errors -
fmt.Errorf("...: %w", err)to add context while preserving the original error;var ErrUserNotFound = errors.New(...)pluserrors.Is(err, ErrUserNotFound)to let callers branch on error kind without string-matching messages. - Dependency injection via structs -
AuthHandler{userRepo, sessions, logger}instead of global variables, so every handler's requirements are explicit and visible in its constructor.
Suggested next steps
If you want to keep extending this project as further practice:
- Testing -
httptest.NewRequest/NewRecorderfor handler tests, table-driven test cases, and extracting aUserStoreinterface soUserRepositorycan be swapped for an in-memory fake in tests. - A real migration tool (e.g.
golang-migrate/migrate) instead ofCREATE TABLE IF NOT EXISTSon every boot. - CSRF tokens if you ever add a same-origin HTML form frontend
(the current
SameSite=Laxcookie already covers the JSON-API case). - Refresh/renewal so an active user's session doesn't hard-expire after 24 hours regardless of activity.
- Machine-readable error codes (
{"error_code": "invalid_credentials"}) so a frontend can branch on a stable code instead of parsing message text. - Grafana Alloy + Loki - point Alloy at this container's stdout; the
JSON shape from
internal/loggingandinternal/middleware/request_logger.gois already structured for it.