A production-oriented starter for Go REST APIs. Opinionated project layout, batteries included: Echo for HTTP routing, PostgreSQL for persistence, RabbitMQ for messaging, InfluxDB for time-series business events, Prometheus for metrics, and JWT/OAuth2 for auth.
Clone it, rename the module, and start adding domains.
| Concern | Choice |
|---|---|
| HTTP | Echo v4 |
| Database | PostgreSQL 15 via go-pg v9 |
| Messaging | RabbitMQ (streadway/amqp) |
| Time-series | InfluxDB 2.x |
| Metrics | Prometheus (+ echo-prometheus) |
| Logging | zerolog (+ lecho) |
| Auth | JWT, OAuth2, zxcvbn password strength |
| API docs | Swagger (swaggo/echo-swagger) |
| RPC/schema | Protocol Buffers |
| Validation | go-playground/validator |
- Go 1.20+
- Docker and Docker Compose
The stack runs PostgreSQL, InfluxDB, and RabbitMQ alongside the app, so it has high RAM requirements. You will want a strong machine or VM.
start-server.sh installs protobuf-compiler via apt, so it assumes Debian/Ubuntu (or WSL). On macOS, install protobuf with brew install protobuf and run the Docker steps by hand.
Configure your environment before running the app. To start from the default config:
cp .env.example .envbash start-server.shThis builds the soapstone-image container, brings the Compose stack up, installs the protobuf toolchain, and builds the Go binary. Kill it with Ctrl+C as normal.
Connect to PostgreSQL:
docker exec -it -u postgres postgresDB psqlConnect to InfluxDB: browse to http://localhost:8086 (credentials come from the INFLUXDB_* variables in your .env).
-
The root directory contains things not related to code directly — Docker Compose, CI/CD, readme, bash scripts.
-
The
cmdpackage contains code for starting applications (main packages). The directory name for each application should match the name of the executable you want. An application may produce multiple binaries, so we follow the Go convention of placing the main package as a subdirectory ofcmd. In a scheduler application, for example, the binary would live undercmd/cron. It also loads configuration and passes it to the service initializers. -
The rest of the code lives under
/pkg, which containsutland microservice directories. -
Microservice directories such as
api(naming corresponds tocmd/naming) contain a folder per domain it interacts with:user,car,appointment, and so on. -
Domain directories such as
usercontain all application/business logic plus two additional directories,platformandtransport. -
platformcontains packages providing support for databases, authentication, and marshaling. Most packages underplatformare decoupled behind interfaces. Every platform gets its own package:postgres,elastic,redis,memcache. -
transportcontains HTTP handlers. The package receives requests, marshals and validates them, then passes them to the corresponding service. -
utlcontains helper packages and models — mock, middleware, configuration, server.
Say you have a cars table for employees' cars. To implement CRUD on it:
-
Inside
pkg/utl/model, createcar.go. Put your entity (struct) there, along with any methods on it. -
Create a
carfolder in the (micro)service where your service will live, most likely underapi. Inside, create the service and its test (car/car.goandcar/car_test.go). You can test without writing a single query by mocking the database logic in/mock/mockdb. If you have complex queries touching other entities, add files likecar_users.goorcar_templates.gohere. -
Inside the
carfolder, createplatform,transport, andlogging. -
Code interacting with a platform such as PostgreSQL goes under
car/platform/pgsql(pkg/api/car/platform/pgsql/car.go). -
In
pkg/api/car/transport, createhttp.go— this is where your handlers live. Addhttp_test.goalongside it to test the API. -
In
logging, createcar.goand copy the logic from another service. This handles request/response logging. -
In
pkg/api/api.go, wire everything up: instantiate the car service, then pass it to the logging and transport services.
Similar to implementing APIs that rely only on a database:
-
In the service package, in
car.go, add an interface corresponding to the platform —Indexer,Reporter, etc. -
The rest of the procedure is the same, except that under
/platformyou create a new folder for your platform, for exampleelastic. -
Once the platform logic is implemented, create an instance of it in
main.go(for exampleelastic.Client) and pass it as an argument to the car service (pkg/api/car/car.go).
Before interacting with the database, create a new transaction:
err := s.db.RunInTransaction(func(tx *pg.Tx) error {
// Application service here
})Instead of passing the database client as s.db, pass tx inside this function. Handle the error accordingly.
We loosely follow a trunk-based protocol.
See LICENSE.