# go-simple-api A small, heavily-commented Go REST API built as a learning project. It implements user authentication two ways - email/password and "Sign in with Google" - using server-side sessions stored in Redis, backed by MySQL for user data. This project was built incrementally, lesson by lesson, specifically to teach Go web-service fundamentals. Every file has generous inline comments explaining *why* the code is written the way it is, not just what it does. See [`docs/LESSONS.md`](docs/LESSONS.md) for the full course this project was built from, and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for a deeper explanation of how the pieces fit together. ## Stack | Concern | Technology | |---|---| | HTTP routing | [chi](https://github.com/go-chi/chi) v5 | | Structured logging | `log/slog` (Go standard library), JSON output | | Database | MySQL, via `database/sql` + `go-sql-driver/mysql` | | Sessions | [scs](https://github.com/alexedwards/scs) v2, backed by Redis | | Password hashing | `golang.org/x/crypto/bcrypt` | | Google login | `golang.org/x/oauth2` (Authorization Code flow) | | Rate limiting | [httprate](https://github.com/go-chi/httprate) | | CORS | [go-chi/cors](https://github.com/go-chi/cors) | ## Project layout ``` go-simple-api/ ├── cmd/api/main.go # entrypoint: wires everything, runs the server ├── internal/ │ ├── config/ # env var loading │ ├── logging/ # structured JSON logger (slog) │ ├── database/ # MySQL connection + migrations │ ├── models/ # User struct + UserRepository (all SQL lives here) │ ├── session/ # scs session manager, backed by Redis │ ├── oauth/ # Google oauth2.Config builder │ ├── handlers/ # HTTP handlers (health, auth, google oauth) │ ├── middleware/ # request logging + auth-guard middleware │ └── router/ # wires routes + middleware together ├── docs/ # architecture, API reference, course notes ├── Dockerfile # multi-stage build ├── docker-compose.yml # app + MySQL + Redis ├── .env.example # every config variable, documented └── go.mod ``` ## Running locally (without Docker) Requires Go 1.26+, and MySQL + Redis reachable somewhere. ```bash # 1. Start MySQL and Redis (or point at existing instances) docker run --name mysql-api -e MYSQL_ROOT_PASSWORD=devpass \ -e MYSQL_DATABASE=go_simple_api -p 3306:3306 -d mysql:9 docker run --name redis-api -p 6379:6379 -d redis:8 # 2. Set up environment cp .env.example .env # edit .env - at minimum, fill in GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET # if you want to test Google login. Password login works without them. export $(grep -v '^#' .env | xargs) # or use a tool like direnv # 3. Fetch dependencies (go.sum is not included - see go.mod for why) go mod tidy # 4. Run go run ./cmd/api ``` The server listens on `:8080` by default. Try: ```bash curl http://localhost:8080/health ``` ## Running with Docker Compose (recommended) ```bash cp .env.example .env # fill in GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET if you want Google login docker compose up --build ``` This starts the API, MySQL, and Redis together, with the API waiting on the other two. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how service networking works inside Compose. Stop everything: ```bash docker compose down # stops containers, keeps the MySQL volume docker compose down -v # also wipes the MySQL volume (fresh start) ``` ## API reference See [`docs/API.md`](docs/API.md) for every endpoint, request/response shapes, and example `curl` commands. Quick overview: | Method | Path | Auth required? | Purpose | |---|---|---|---| | GET | `/health` | no | Liveness check | | POST | `/register` | no | Create a password-based account | | POST | `/login` | no | Log in with email + password, starts a session | | POST | `/logout` | no (needs a session to destroy) | Ends the current session | | GET | `/me` | **yes** | Returns the currently logged-in user | | GET | `/auth/google/login` | no | Redirects the browser to Google | | GET | `/auth/google/callback` | no | Google redirects here after login | ## Google OAuth setup 1. Go to the [Google Cloud Console credentials page](https://console.cloud.google.com/apis/credentials). 2. Create an **OAuth 2.0 Client ID** (Application type: Web application). 3. Add an **Authorized redirect URI**: `http://localhost:8080/auth/google/callback` (must match `GOOGLE_REDIRECT_URL` in your `.env` exactly). 4. Copy the Client ID and Client Secret into `.env`. ## Security notes This project deliberately implements several production-appropriate security practices, explained in detail in the code comments where they appear: - Passwords are hashed with bcrypt, never stored or logged in plaintext. - Login returns an identical, generic error for both "no such email" and "wrong password", to avoid leaking which emails are registered. - Sessions are server-side (Redis-backed) - the browser only ever holds a random token, never the actual session data. - `sessions.RenewToken()` is called on every successful login (password or Google) to prevent session fixation. - The session cookie is `HttpOnly` (JS can't read it) and `SameSite=Lax` (mitigates CSRF); `Secure` is enabled automatically when `ENV=production`. - The OAuth2 flow uses a random `state` value, checked on callback, to prevent CSRF against the login flow itself. - `/login` and `/register` have a much stricter rate limit than the rest of the API, to slow down credential-stuffing / brute-force attempts. - CORS is an explicit origin allowlist (`ALLOWED_ORIGINS`), never a wildcard, since the API uses credentialed (cookie-based) requests. ## What's not included (possible next steps) - Automated tests (`httptest`, table-driven tests, a mockable repository interface) - A real migration tool (e.g. `golang-migrate`) instead of `CREATE TABLE IF NOT EXISTS` - CSRF tokens for a same-origin HTML form frontend (SameSite=Lax already covers the cookie-based JSON API case) - Refresh/renewal flow for sessions beyond their fixed 24h lifetime - Machine-readable error codes in API responses (currently just a message string) - Shipping logs to Grafana Loki via Grafana Alloy (the JSON log shape produced by this app is already Alloy/Loki-friendly - see `internal/logging` and `internal/middleware/request_logger.go`)