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

  1. Add path + operation to spec/api.yaml
  2. Add request/response schemas to components/schemas
  3. Run make generate or make generate-strict
  4. 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