A Go REST API built contract first. The OpenAPI spec generates the server interface and a client, and the SQL queries generate the database code. You write only the handlers and the repository that connect the two.
Two generators remove most of the hand-written code in a CRUD API:
| Generator | Reads | Writes |
|---|---|---|
| oapi-codegen | api/openapi.yml |
internal/gen/oapi/: the models and a strict Fiber server interface. pkg/gen/: a typed client |
| sqlc | db/migrations/ and db/queries/users.sql |
internal/gen/db/: typed Go functions for each query, on pgx |
The strict server interface has one method for each operation, with typed request and response objects. A handler cannot return a response that the spec does not declare: the compiler rejects it.
The generated code is not committed. mise run generate writes it, and mise run setup and mise run ci call it.
-
Install the tools, generate the code, and start Postgres:
git clone git@github.com:tabmadi/go-oapi-sqlc-example.git cd go-oapi-sqlc-example mise install mise run setup cp .env.example .env mise run db:up mise run migrate -
Start the API, then call it:
mise run serve curl -s -XPOST localhost:8080/users -H 'Content-Type: application/json' -d '{"name":"Ada","email":"ada@example.com"}' curl -s 'localhost:8080/users?limit=10'
| Method | Path | Does |
|---|---|---|
GET |
/users |
Lists users. name and email filter, and limit and offset page |
POST |
/users |
Creates a user |
GET |
/users/{id} |
Gets one user |
PUT |
/users/{id} |
Updates a user |
DELETE |
/users/{id} |
Deletes a user |
api/openapi.yml is the full contract.
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
The Postgres connection string. It is required | none |
LISTEN_ADDRESS |
The address the server binds | localhost:8080 |
READ_TIMEOUT |
The request read timeout | 15s |
WRITE_TIMEOUT |
The response write timeout | 15s |
IDLE_TIMEOUT |
The keep-alive idle timeout | 60s |
SHUTDOWN_TIMEOUT |
The time a shutdown waits for open requests | 5s |
| File | Holds |
|---|---|
api/openapi.yml |
The contract: paths, parameters, and schemas |
db/migrations/ |
The schema, applied by golang-migrate. sqlc reads it too |
db/queries/users.sql |
The queries, with sqlc annotations |
configs/ |
The generator, linter, and SQL formatter configs |
internal/repository/ |
Maps between the API models and the database rows |
internal/server/server.go |
The handlers that implement the strict server interface |
internal/config/config.go |
Reads the configuration from the environment and .env |
cmd/api/main.go |
Wires the parts and starts the server |
- Tests: the example has none. The generated interface makes the handlers easy to test with a fake repository.
- Safe error messages: a
500response carries the database error text. A production API logs the error and returns a generic message. - Validation from the spec: the server does not check requests against the OpenAPI schema. A request validator middleware adds it.
- Auth, rate limits, and structured logs.
| Task | Does |
|---|---|
mise run setup |
Downloads the modules, generates the code, and installs the git hooks |
mise run generate |
Generates the server, the client, and the queries |
mise run db:up |
Starts the local Postgres. db:down stops it and deletes its data |
mise run migrate |
Applies the migrations |
mise run serve |
Runs the API from source |
mise run build |
Builds a static binary at bin/api |
mise run lint |
Runs lint:go, lint:sql, lint:md, and lint:secrets |
mise run format |
Applies every formatter |
mise run check |
Runs lint and test. Run it before you finish a change |
mise run ci |
Runs exactly what CI runs. The pre-push hook runs it too |
Apache License 2.0. See LICENSE.