A production-grade REST API for managing a collection of books, built in Go with Fiber and GORM on SQLite. It demonstrates a clean, layered architecture, graceful shutdown, structured logging, health checks, containerization, and CI.
- Layered architecture —
handler → service → repositorywith interfaces for testability - Graceful shutdown — listens for
SIGINT/SIGTERM, drains in-flight requests with a bounded timeout - Structured logging — JSON logs via
log/slog, per-request log lines with request IDs - Health checks —
/healthzprobe that pings the database - Middleware — request ID, panic recovery, CORS, request logging
- Optional API-key auth — guards write endpoints when configured
- Input validation & pagination — on all relevant endpoints
- Consistent JSON error envelope — uniform
{error, code}responses - Config from environment — validated on startup
- Containerized — multi-stage Dockerfile + docker-compose
- CI — GitHub Actions: format check, vet, race tests, lint, Docker build
- OpenAPI spec — see
api/openapi.yaml
| Component | Library | Version |
|---|---|---|
| HTTP framework | github.com/gofiber/fiber/v2 |
v2.52.13 |
| ORM | gorm.io/gorm |
v1.31.1 |
| DB driver | gorm.io/driver/sqlite |
v1.6.0 |
| Logging | log/slog (stdlib) |
— |
| Language | Go | 1.24+ |
Requests flow through clearly separated layers, each depending only on the one below it via interfaces:
HTTP request
│
▼
┌──────────┐ parse, map errors, status codes
│ Handler │ internal/book/handler.go
└────┬─────┘
▼
┌──────────┐ business rules: normalize, validate, orchestrate
│ Service │ internal/book/service.go
└────┬─────┘
▼
┌──────────┐ persistence (Repository interface)
│Repository│ internal/book/repository.go → GORM / SQLite
└──────────┘
The Repository interface lets the service be unit-tested with an in-memory fake
(no database required), while the full stack is covered by integration tests.
.
├── cmd/
│ └── server/
│ └── main.go # entry point: config, logger, DB, server, graceful shutdown
├── internal/
│ ├── config/ # env-var loading + validation
│ ├── database/ # GORM/SQLite open, pool tuning, ping, migrate
│ ├── book/ # domain: model, repository, service, handler
│ ├── server/ # Fiber app builder + middleware
│ └── response/ # JSON error envelope
├── api/
│ └── openapi.yaml # OpenAPI 3 spec
├── .github/workflows/ci.yml
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── .golangci.yml
├── .env.example
└── go.mod / go.sum
- Go 1.24+ (
go version) - A C compiler — the SQLite driver (
mattn/go-sqlite3) uses cgo, soCGO_ENABLED=1(default on macOS/Linux with Xcode CLT or gcc) - Optional: Docker,
make,golangci-lint
git clone https://github.com/sohag-pro/rest-api-with-go.git
cd rest-api-with-go
# optional: set up environment (defaults work without it)
cp .env.example .env
export $(grep -v '^#' .env | xargs)
make run # or: go run ./cmd/serverThe server listens on :3000, creates the SQLite file, and migrates the schema
on first run.
docker compose up --build
# or
make docker && docker run -p 3000:3000 books-api:latestAll settings come from environment variables (validated on startup). See
.env.example.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Listen port (1–65535) |
DB_PATH |
books.db |
SQLite file path |
API_KEY |
(empty) | If set, write endpoints require it; empty disables auth |
LOG_LEVEL |
info |
debug | info | warn | error |
READ_TIMEOUT |
10s |
HTTP read timeout |
WRITE_TIMEOUT |
10s |
HTTP write timeout |
SHUTDOWN_TIMEOUT |
10s |
Graceful shutdown grace period |
Read endpoints (GET) are always public. Write endpoints (POST, PATCH,
DELETE) require the X-API-Key header only when API_KEY is set:
curl -X POST localhost:3000/api/v1/book \
-H 'X-API-Key: s3cret' \
-H 'Content-Type: application/json' \
-d '{"title":"Go","rating":5}'Base path: http://localhost:3000
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /healthz |
Liveness/readiness probe | — |
| GET | /api/v1/book |
List books (paginated) | — |
| GET | /api/v1/book/:id |
Get one book by ID | — |
| POST | /api/v1/book |
Create a book | key |
| PATCH | /api/v1/book/:id |
Update a book | key |
| DELETE | /api/v1/book/:id |
Delete a book | key |
Full schema in api/openapi.yaml.
{
"title": "Clean Code",
"author": "Robert C. Martin",
"rating": 5
}Responses also include GORM metadata: ID, CreatedAt, UpdatedAt, DeletedAt.
title— required, whitespace-trimmed, non-emptyrating— integer between0and5
GET /api/v1/book accepts query parameters:
| Param | Default | Notes |
|---|---|---|
limit |
10 |
Max 100; out-of-range falls back to 10 |
offset |
0 |
Negative clamps to 0 |
curl 'localhost:3000/api/v1/book?limit=20&offset=40'All errors return a consistent JSON shape:
{ "error": "book not found", "code": 404 }| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Validation failed / invalid id |
| 401 | Missing/invalid X-API-Key (when auth enabled) |
| 404 | Not found |
| 406 | Request body could not be parsed |
| 500 | Internal error |
| 503 | Database unavailable (/healthz) |
# Create
curl -X POST localhost:3000/api/v1/book \
-H 'Content-Type: application/json' \
-d '{"title":"Clean Code","author":"Robert C. Martin","rating":5}'
# List (paginated)
curl 'localhost:3000/api/v1/book?limit=10&offset=0'
# Get one
curl localhost:3000/api/v1/book/1
# Update
curl -X PATCH localhost:3000/api/v1/book/1 \
-H 'Content-Type: application/json' \
-d '{"title":"Clean Code","author":"Uncle Bob","rating":4}'
# Delete
curl -X DELETE localhost:3000/api/v1/book/1
# Health
curl localhost:3000/healthzmake help # list all targets
make run # run the server
make test # race-enabled tests
make cover # coverage report (HTML)
make vet # go vet
make fmt # gofmt -w
make lint # golangci-lint
make build # build ./bin/books-api
make docker # build Docker imageTests cover service logic (with an in-memory fake repository), full HTTP integration (handlers → service → repository), validation, pagination, auth, health checks, and config loading. Each test uses an isolated temporary SQLite file — no running server or shared state required.
go test -race ./...MIT © Sohag Hasan