Skip to content

OpenAPI workflow — `oapi-codegen`

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/go-backend-engineer/openapi.md
DescriptionNot specified

Source Content

OpenAPI workflow — oapi-codegen

Spec-first. Hand-write api/openapi.yaml (OpenAPI 3.1), generate types and Fiber server stubs from it. Never the other way round — generated specs from code drift.

Generation targets

Terminal window
# Types (request/response structs, enums)
oapi-codegen -generate types -package api -o internal/api/types.gen.go api/openapi.yaml
# Fiber server interface + route registrar
oapi-codegen -generate fiber -package api -o internal/api/server.gen.go api/openapi.yaml
# Embedded spec for /openapi endpoint
oapi-codegen -generate spec -package api -o internal/api/spec.gen.go api/openapi.yaml

Wired in Taskfile.yml as task oapi:generate. Run on every spec change; the generated files are committed.

Spec conventions

  • OpenAPI 3.1 (full JSON Schema 2020-12 alignment).

  • Errors: RFC 9457 Problem Details — one shared component, every operation references it.

    components:
    schemas:
    Problem:
    type: object
    required: [type, title, status]
    properties:
    type: { type: string, format: uri, default: "about:blank" }
    title: { type: string }
    status: { type: integer }
    detail: { type: string }
    instance: { type: string, format: uri }
  • IDs: { type: string, format: uuid }. Use x-go-type-skip-optional-pointer: true to keep generated structs tidy.

  • Pagination: cursor-based by default — ?limit=50&cursor=abc returning { items, next_cursor }. Never offset for collections expected to grow past 10k.

  • Versioning: URL prefix (/v1, /v2). New required fields = new version.

  • Operation IDs: <verb><Resource> e.g. createUser, listUsers, getUser.

Implementing a generated handler

internal/api/server.go
type Server struct {
userSvc service.UserService
}
func NewServer(s service.UserService) *Server { return &Server{userSvc: s} }
// CreateUser implements api.ServerInterface (generated).
func (s *Server) CreateUser(c *fiber.Ctx) error {
var req api.CreateUserRequest
if err := c.BodyParser(&req); err != nil {
return problem(c, http.StatusBadRequest, "invalid_body", err)
}
user, err := s.userSvc.Create(c.UserContext(), &service.CreateUserRequest{
Email: req.Email, Name: req.Name, Role: string(req.Role), TenantID: req.TenantId.String(),
})
if err != nil {
return mapDomainErr(c, err)
}
return c.Status(http.StatusCreated).JSON(api.UserResponse{
Id: openapi_types.UUID(uuid.MustParse(user.ID)),
Email: user.Email,
})
}

Validation

  • Schema validation happens at decode time via the generated types’ oapi-codegen validator middleware (oapimiddleware.OapiRequestValidatorWithOptions).
  • Business rules (uniqueness, ownership) live in the service layer.

Mock server for parallel FE/BE work

Terminal window
prism mock api/openapi.yaml --port 4010

The frontend can develop against the spec before a single handler is implemented. Pair with Bruno or Hurl for API contract tests checked into the repo.

CI checks

CheckCommand
Spec lintspectral lint api/openapi.yaml --ruleset .spectral.yaml
Spec validoapi-codegen -package api api/openapi.yaml > /dev/null
Generated code in synctask oapi:generate && git diff --exit-code internal/api/*.gen.go
Backwards compatoasdiff breaking api/openapi.yaml@HEAD~1 api/openapi.yaml@HEAD