Skip to content

Repository files navigation

Bookstore API

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.

Swagger UI

Highlights

  • 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-After headers.
  • 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.

Quick start

Requires Docker.

cp .env.example .env        # then set JWT_SECRET and ADMIN_PASSWORD
docker compose up --build

On first start the catalogue is seeded and the admin account from .env is created.

Try it

# 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" }
  }
}

Endpoints

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.

Design notes

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.

Configuration

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.

Development

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 docs

Integration 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.

Project structure

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

About

Production-style Go REST API: JWT with refresh-token rotation, RBAC, MongoDB, Redis caching and rate limiting, Swagger, Docker

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages