A production-style REST API for a book catalogue, written in Go. It covers the pieces most real backends need: authentication with rotating refresh tokens, role-based access, filtering and pagination, Redis caching, rate limiting, validation, structured logging, OpenAPI docs, and a one-command Docker setup.
It ships with 1,000 real books, scraped by my book-scraper project, so every endpoint has data to show on first start.
- JWT auth with refresh-token rotation: short-lived access tokens plus opaque refresh tokens stored in Redis. Each refresh token works exactly once, and logout revokes it immediately.
- Role-based access: anyone can browse; only admins can create, update, or delete books.
- Filtering, search, sorting, pagination with every invalid parameter reported at once.
- Redis caching with O(1) invalidation: any write invalidates every cached list without scanning keys. If Redis goes down, reads fall back to MongoDB instead of failing.
- Rate limiting on auth endpoints, shared across replicas through Redis, with
Retry-Afterheaders. - Consistent JSON errors with machine-readable codes and per-field validation messages.
- Structured JSON logs with request IDs, graceful shutdown, health checks.
- Tested against real MongoDB and Redis, not mocks.
- Small, hardened image: 44 MB, distroless, runs as non-root.
Requires Docker.
cp .env.example .env # then set JWT_SECRET and ADMIN_PASSWORD
docker compose up --build- API: http://localhost:8080/api/v1
- Interactive docs: http://localhost:8080/docs
- Health: http://localhost:8080/healthz
On first start the catalogue is seeded and the admin account from .env is created.
# Top-rated travel books
curl "http://localhost:8080/api/v1/books?category=Travel&sort=-rating&limit=2"
# Log in as admin and add a book
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"<ADMIN_PASSWORD>"}' | jq -r .access_token)
curl -X POST http://localhost:8080/api/v1/books \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"upc":"gopl2015","title":"The Go Programming Language","category":"Programming","price_cents":3999,"currency":"GBP","rating":5,"stock":7}'Errors always have the same shape:
{
"error": {
"code": "validation_failed",
"message": "request validation failed",
"fields": { "limit": "must be an integer between 1 and 100", "sort": "must be one of title, -title, price, -price, rating, -rating" }
}
}| Method | Path | Access | Description |
|---|---|---|---|
| POST | /api/v1/auth/register |
public, rate limited | Create an account, returns user and tokens |
| POST | /api/v1/auth/login |
public, rate limited | Returns an access/refresh token pair |
| POST | /api/v1/auth/refresh |
public, rate limited | Rotates the token pair |
| POST | /api/v1/auth/logout |
public | Revokes a refresh token |
| GET | /api/v1/me |
signed in | Current user |
| GET | /api/v1/books |
public | List with q, category, min_price, max_price, min_rating, in_stock, sort, page, limit |
| GET | /api/v1/books/{id} |
public | One book |
| POST | /api/v1/books |
admin | Create a book |
| PATCH | /api/v1/books/{id} |
admin | Update only the fields sent |
| DELETE | /api/v1/books/{id} |
admin | Delete a book |
| GET | /api/v1/categories |
public | Categories with book counts |
| GET | /healthz |
public | 200 when MongoDB and Redis respond, 503 otherwise |
List and category responses carry an X-Cache: HIT|MISS header.
Refresh tokens are server-side. A JWT alone cannot be revoked, so refresh tokens are random strings stored in Redis with a TTL. Only their SHA-256 hash is stored, so a Redis dump leaks nothing usable. Refreshing uses an atomic GETDEL, which guarantees a token is accepted once even under concurrent requests.
Login does not leak which emails exist. Unknown emails still run a bcrypt comparison, so both failure paths take the same time and return the same message.
Versioned cache keys. Cache keys contain a version number, and every write increments it. That invalidates all cached pages in one Redis command, with no KEYS/SCAN; stale entries simply expire.
Money is stored as integer cents (price_cents) to avoid floating-point rounding errors. Filters accept decimal input such as min_price=10.50.
Search is injection-safe. Title search uses a case-insensitive regex built with regexp.QuoteMeta, so user input is always matched literally.
Set through environment variables (see .env.example).
| Variable | Default | Description |
|---|---|---|
JWT_SECRET |
required | Signing key, at least 32 characters |
ADMIN_EMAIL / ADMIN_PASSWORD |
unset | Admin account created on first start |
PORT |
8080 |
HTTP port |
MONGO_URI / MONGO_DB |
mongodb://localhost:27017 / bookstore |
MongoDB connection |
REDIS_ADDR / REDIS_PASSWORD / REDIS_DB |
localhost:6379 / empty / 0 |
Redis connection |
ACCESS_TOKEN_TTL |
15m |
Access token lifetime |
REFRESH_TOKEN_TTL |
168h |
Refresh token lifetime |
CACHE_TTL |
5m |
Cache entry lifetime |
AUTH_RATE_LIMIT |
10 |
Auth requests per minute per IP |
SEED_BOOKS |
true |
Seed the catalogue when it is empty |
Invalid values are all reported together at startup.
Requires Go 1.26+.
docker compose up -d mongo redis # dependencies only
go run ./cmd/api # with the variables from .env exported
go test ./... # unit tests
go test -tags integration -count=1 ./... # integration tests against real MongoDB and Redis
swag init -g cmd/api/main.go -o docs --parseInternal --outputTypes go,json # regenerate OpenAPI docsIntegration tests start the whole application against a throwaway MongoDB database and Redis DB 15, and cover registration, login, token rotation, logout, rate limiting, filtering, caching, admin permissions, and cache invalidation.
cmd/api/ entry point, graceful shutdown
internal/
app/ wiring + integration tests
auth/ JWT, refresh tokens, login/registration, auth middleware
book/ catalogue: model, query parsing, repository, cache layer, handlers
config/ environment configuration
httpx/ error format, validation, request logging, rate limiting
platform/ MongoDB and Redis connections
seed/ demo catalogue (1,000 books)
server/ routes, middleware stack, Swagger UI
user/ user model and repository
docs/ generated OpenAPI spec
