Overview
Two generators run in sequence against a single OpenAPI spec:
| Generator | Owns | Output |
|---|---|---|
oapi-codegen |
Types, enums, nested structs, optional pointers | generated/types.go |
gen/main.go |
Per-tag interfaces, route registration, strict glue | generated/interfaces.go, routes.go, routes.go, requests.go |
The result is a clean boundary: generated code you never touch, and handler files you fully own.
Prerequisites
make install-tools
Installs oapi-codegen into $(go env GOPATH)/bin.
Project layout
spec/
api.yaml ← source of truth, edit this
cfg/
types.yaml ← oapi-codegen config (types only)
gen/
main.go ← custom splitter — generates per-tag interfaces + routes
generated/ ← never edit, safe to delete and rebuild
types.go
interfaces.go
routes.go
requests.go ← strict mode only
server/
middleware/ ← hand-written
handlers/ ← hand-written, implement generated interfaces
server.go ← wires router + middleware + handlers
Makefile
Usage
Standard mode
Handlers receive *gin.Context directly. Full manual control.
make generate
Generated interface:
type UsersHandler interface {
GetUser(c *gin.Context)
CreateUser(c *gin.Context)
DeleteUser(c *gin.Context)
}
Strict mode
Handlers receive typed request objects and return typed responses. Binding and response encoding are generated.
make generate-strict
Generated interface:
type OrdersHandler interface {
// GetOrder path: orderId → Order
GetOrder(ctx context.Context, request GetOrderRequestObject) (*Order, error)
// CreateOrder body: CreateOrderRequest → Order
CreateOrder(ctx context.Context, request CreateOrderRequestObject) (*Order, error)
}
Implementing a handler (strict)
// server/handlers/orders.go
type OrdersHandler struct{ db *sqlx.DB }
func (h *OrdersHandler) GetOrder(ctx context.Context, req generated.GetOrderRequestObject) (*generated.Order, error) {
order, err := h.db.FindOrder(ctx, req.OrderId)
if errors.Is(err, ErrNotFound) {
return nil, &generated.HTTPError{
Code: http.StatusNotFound,
Body: generated.ErrorResponse{Code: "NOT_FOUND", Message: "order not found"},
}
}
if err != nil {
return nil, err // → 500
}
return order, nil // → 201 with Order body
}
Error responses
Return *HTTPError to control status code and body. Any other error maps to 500.
// typed error with body
return nil, &generated.HTTPError{Code: 404, Body: generated.ErrorResponse{...}}
// untyped → 500
return nil, fmt.Errorf("db unavailable")
Wiring the server
// server/server.go
r := gin.New()
r.Use(middleware.Logging(), middleware.Auth())
v1 := r.Group("/v1")
generated.RegisterUsersRoutes(v1, &handlers.UsersHandler{db: db})
generated.RegisterOrdersRoutes(v1, &handlers.OrdersHandler{db: db})
generated.RegisterItemsRoutes(v1, &handlers.ItemsHandler{db: db})
Adding a new endpoint
- Add path + operation to
spec/api.yaml - Add request/response schemas to
components/schemas - Run
make generateormake generate-strict - Implement the new method on the relevant handler struct — compiler will enforce it via the interface
Moving to another project
Copy these four files:
gen/main.go
cfg/types.yaml
Makefile
Then:
1. Update go.mod dependency
gen/main.go requires gopkg.in/yaml.v3:
go get gopkg.in/yaml.v3
2. Update cfg/types.yaml
Change the package name if needed:
package: generated # ← change to match your package
output: generated/types.go
generate:
models: true
embedded-spec: false
gin-server: false
client: false
strict-server: true
3. Update gen/main.go — package name
Near the bottom of main(), change the hardcoded package:
data := TemplateData{
Package: "generated", // ← change if your output package differs
...
}
4. Adjust paths in Makefile if needed
generate:
$(OAPI_CODEGEN) --config cfg/types.yaml spec/api.yaml # ← spec path
go run ./gen spec/api.yaml generated # ← spec path, output dir
go build ./...
5. Add go:generate directive (optional)
In your main.go or server.go:
//go:generate make generate-strict
Then go generate ./... runs the full pipeline.
How oapi-codegen handles types
Feature in spec → Go output:
| Spec | Go |
|---|---|
required: [name] |
Name string |
| optional field | Name *string |
format: date-time |
*time.Time |
format: double |
float64 |
enum: [a, b, c] |
typed string alias + constants + Valid() |
$ref to schema |
named struct |
array of $ref |
[]TypeName |
gen/main.go flags
go run ./gen [--strict] [spec-path] [output-dir]
--strict generate strict interfaces + gin glue + requests.go
spec-path default: spec/api.yaml
output-dir default: generated