Skip to content

ferminhg/gophercraft

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

37 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

gophercraft

codecov

A Go project template built with care 🐹 It comes with Hexagonal Architecture, CQRS, and observability out of the box (structured logs and Prometheus /metrics) β€” so you can skip the boring setup and focus on what matters: writing great code ✨ Just clone, configure, and start crafting.

Getting started

You need a recent Go toolchain (this repo targets Go 1.26). Clone the repository, then download modules and run the API entrypoint:

go mod download
make run

The API listens for HTTP (by default on :3000; override with HTTP_ADDR). A minimal GET /status route answers 200 with {"status":"ok"} so you can verify the stack quickly. GET /metrics exposes Prometheus text format for scraping or for wiring Grafana with Prometheus as a datasource β€” see Metrics (Prometheus).

Logging

The service uses structured logging so you avoid ad-hoc fmt/log at the edges and keep output consistent for operators and log aggregators.

Piece Location Role
Port internal/domain/port/logger.go Logger β€” framework-agnostic API (Info, Warn, Error, Debug, Fatal, With). Args are alternating key-value pairs (same idea as log/slog).
Adapter internal/infrastructure/logger zerolog implementation: JSON lines by default, optional human-readable console output in development.
HTTP internal/infrastructure/handler/middleware_logger.go Replaces Gin’s default request logger with a line per request (method, path, status, latency_ms).

At startup, cmd/api/main.go builds the adapter and injects it into the HTTP server. Use cases can accept port.Logger later the same way.

Environment variables

Variable Default Meaning
LOG_LEVEL info Zerolog level: trace, debug, info, warn, error, fatal, panic, or disabled. Invalid values fall back to info.
LOG_PRETTY (off) Set to true or 1 for colored, multi-field console lines instead of JSON. Use this for local or docker compose terminals; keep JSON (LOG_PRETTY unset) in production so Loki, ELK, Cloud Logging, etc. can parse one object per line.
HTTP_ADDR :3000 Listen address for the HTTP server.
OTEL_SERVICE_NAME β€” If set, becomes service.name on every log line (OpenTelemetry resource).
SERVICE_NAME β€” Used as service.name when OTEL_SERVICE_NAME is empty.
(implicit default) gophercraft service.name when neither of the above is set.
DEPLOYMENT_ENVIRONMENT β€” If set, becomes deployment.environment (OTel-style).
ENV / APP_ENV β€” Fallback for deployment.environment when DEPLOYMENT_ENVIRONMENT is empty.

In Docker or Kubernetes, JSON + service.name + deployment.environment is the usual pattern so you can filter and correlate logs across services.

Metrics (Prometheus)

The HTTP server exposes a GET /metrics endpoint in Prometheus exposition format, so a Prometheus server can scrape it and Grafana can chart request rates and latencies.

Piece Location Role
Port internal/domain/port/metrics_recorder.go MetricsRecorder β€” records HTTP request samples (method, normalized route, status, duration in seconds) without tying the domain to Prometheus.
Adapter internal/infrastructure/metrics/prometheus_recorder.go prometheus/client_golang implementation: dedicated registry (not the global default), suitable for tests.
No-op internal/infrastructure/metrics/noop_recorder.go Drops samples β€” used in HTTP tests and anywhere you do not need metrics.
HTTP internal/infrastructure/handler/http.go metricsMiddleware emits per-request metrics; /metrics is served with promhttp.HandlerFor when the composition root supplies a prometheus.Gatherer.

Exported series (application HTTP traffic; GET /metrics itself is not counted):

Metric Type Labels
http_requests_total Counter method, route, status_code
http_request_duration_seconds Histogram method, route

At startup, cmd/api/main.go constructs PrometheusRecorder, injects it into the server as both MetricsRecorder (middleware) and Gatherer (scraping). To try locally after make run:

curl -s http://localhost:3000/metrics | head

Project structure

The layout follows hexagonal architecture (ports and adapters) with three main areas under internal/:

Path Role
internal/domain Domain: entities, value objects, and ports (interfaces) that describe what the application needs from the outside world β€” including port.Logger for structured logs and port.MetricsRecorder for HTTP metrics.
internal/application Application: use cases. Commands and queries are separated to keep a CQRS-friendly shape.
internal/infrastructure Infrastructure: adapters β€” HTTP handlers (driving), logging (zerolog), and repositories (driven) that implement the domain ports.

The cmd/api package is the composition root: it wires concrete adapters to handlers. Shared, stable helpers can live in pkg/ when they are safe to import from other modules.

Running tests

Run the full test suite with the race detector:

make test

Tests use testify for assertions:

  • github.com/stretchr/testify/require β€” fatal checks (t.FailNow). Use for preconditions and setup (for example constructing a server or decoding a response) so the test stops immediately on failure.
  • github.com/stretchr/testify/assert β€” non-fatal checks. Use for comparing values (status codes, fields) when you still want clearer failures in one place.

Docs: testify on pkg.go.dev.

There is a small dummy test under internal/domain/model, HTTP and middleware tests under internal/infrastructure/handler, zerolog adapter tests under internal/infrastructure/logger, and Prometheus recorder tests under internal/infrastructure/metrics so CI exercises the stack end to end.

Dummy API routes

The Dummy example wires CQRS end to end: command.CreateDummyHandler handles writes and query.GetDummyHandler handles reads. The HTTP adapter in internal/infrastructure/handler exposes them as:

Method Route Use case Success
POST /dummies Create 201 Created (empty body)
GET /dummies/:id Get by UUID 200 OK with JSON body

Request / validation rules

  • Create (POST /dummies) β€” JSON body with name (non-empty, max 255 chars after trim) and type (one of alpha, beta, gamma).
  • Get (GET /dummies/:id) β€” path id must be a valid UUID. Unknown IDs return 404.

Manual testing (curl)

Start the API, then exercise the routes from another terminal:

make run

Create a Dummy β€” expect 201 and an empty response body (the server assigns a UUID internally):

curl -i -X POST http://localhost:3000/dummies \
  -H 'Content-Type: application/json' \
  -d '{"name":"acme","type":"gamma"}'

Get a Dummy β€” replace {id} with a valid UUID. A random valid UUID that was never created returns 404:

curl -i http://localhost:3000/dummies/550e8400-e29b-41d4-a716-446655440000

Successful GET responses look like:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "acme",
  "type": "gamma",
  "created_at": "2026-05-14T15:30:00Z"
}

Validation errors β€” invalid JSON or domain rules return 400; repository failures return 500:

# invalid JSON β†’ 400
curl -i -X POST http://localhost:3000/dummies \
  -H 'Content-Type: application/json' \
  -d '{invalid'

# empty name β†’ 400
curl -i -X POST http://localhost:3000/dummies \
  -H 'Content-Type: application/json' \
  -d '{"name":"   ","type":"gamma"}'

# unknown type β†’ 400
curl -i -X POST http://localhost:3000/dummies \
  -H 'Content-Type: application/json' \
  -d '{"name":"acme","type":"delta"}'

# malformed id β†’ 400
curl -i http://localhost:3000/dummies/not-a-uuid

POST /dummies does not return the new id in the response yet, so a full create-then-fetch round trip over HTTP requires knowing the generated UUID (for example from structured logs) or relying on the automated tests below, which use a fixed UUID and in-memory repository.

Automated tests

Tests are split by layer, matching hexagonal boundaries:

Layer Package What it covers
Application (command) internal/application/command CreateDummyHandler β€” persistence, event publish, validation (create_dummy_test.go).
Application (query) internal/application/query GetDummyHandler β€” load by ID, not-found (get_dummy_test.go).
HTTP adapter internal/infrastructure/handler Gin handlers β€” status codes, JSON binding, error mapping (create_dummy_test.go, get_dummy_test.go).

Run only the Dummy-related packages:

go test -v -race ./internal/application/command/... ./internal/application/query/... ./internal/infrastructure/handler/...

Or run a single test by name (same pattern as make test):

go test -v -race -run ^TestCreateDummyHandler_Handle_PersistsAndPublishes$ ./internal/application/command
go test -v -race -run ^TestGetDummyHandler_Handle_ReturnsStoredDummy$ ./internal/application/query
go test -v -race -run ^TestCreateDummyGinHandler_Handle_Created$ ./internal/infrastructure/handler
go test -v -race -run ^TestGetDummyGinHandler_Handle_OK$ ./internal/infrastructure/handler

Continuous integration (GitHub Actions)

This repository ships with a GitHub Actions workflow at .github/workflows/ci.yml. It runs on pull requests and on pushes to main, and includes:

  • Testing: go test -v -race ./... (same idea as make test)
  • Linting: golangci-lint using the checked-in .golangci.yml

You get the same checks locally with make test and make lint.

Docker

Build the container image (tag defaults to gophercraft:latest; override with IMAGE):

make docker-build

Run the stack with Docker Compose (uses compose.yml). Create a .env file when you need overrides; you can start from .env.example:

cp .env.example .env   # optional
docker compose up -d --build

The api service maps port 3000 on the host to 3000 in the container. Adjust ports, environment, or .env as needed.

Monitoring stack (Prometheus + Grafana)

The compose.yml file also starts a small monitoring stack next to the API, so you can scrape and chart the /metrics endpoint without any extra setup:

Service Image Host port What it does
prometheus prom/prometheus:latest 9090 Scrapes the API and stores the time series. It reads its config from deploy/prometheus.yml, which is mounted read-only into the container.
grafana grafana/grafana:latest 4000 Dashboards on top of Prometheus. Grafana also listens on 3000 inside the container, so it is mapped to 4000 on the host to avoid a clash with the API. Default login is admin / admin.

Prometheus uses the scrape job defined in deploy/prometheus.yml:

global:
  scrape_interval: 5s

scrape_configs:
  - job_name: "gophercraft"
    metrics_path: /metrics
    static_configs:
      - targets: ["api:3000"]

Because every service shares the Compose network, Prometheus reaches the API by its service name (api:3000), not localhost. The prometheus and grafana services declare depends_on so they start after the API. Grafana data is kept in the grafana-data named volume, so dashboards and settings survive a restart.

Pre-provisioned dashboard

Grafana starts with a pre-provisioned dashboard β€” no manual import needed. The datasource and dashboard are loaded automatically via the files under deploy/grafana/provisioning/:

  • datasources/prometheus.yml β€” registers Prometheus (http://prometheus:9090) as the default datasource.
  • dashboards/gophercraft_overview.json β€” the Gophercraft Overview dashboard with two panels:
    • HTTP request duration β€” p99 / p90 / p75 latency histogram over time.
    • Requests per second β€” per-route throughput derived from http_requests_total.

Gophercraft Overview Grafana dashboard

After docker compose up -d --build, open:

Stop and remove containers for this project:

docker compose down

Makefile targets

Target Description
make build go build ./...
make test go test -v -race ./...
make lint golangci-lint run ./... (requires golangci-lint on your PATH)
make run go run ./cmd/api
make docker-build Builds the Docker image (IMAGE overrides the tag)

Architecture (short overview)

Hexagonal architecture keeps domain rules in the centre. Driving adapters (for example HTTP) call into the application layer. Driven adapters (for example a database or in-memory store) implement ports defined next to the domain. Dependencies point inward, so the domain does not know about frameworks or IO details.

This template separates commands (internal/application/command) from queries (internal/application/query) so you can grow toward CQRS without rewriting the folder layout. Structured logging is wired for HTTP and fatal startup errors; extend it by injecting port.Logger into application handlers as you add behaviour. Prometheus metrics cover HTTP traffic at the adapter layer and can be extended behind port.MetricsRecorder the same way.

About

A Go project template built with care 🐹 It comes with Hexagonal Architecture, CQRS, and observability out of the box β€” so you can skip the boring setup and focus on what matters: writing great code ✨ Just clone, configure, and start crafting.

Resources

License

Stars

Watchers

Forks

Contributors