OpenAPI workflow — `oapi-codegen`
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/go-backend-engineer/openapi.md |
| Description | Not 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
# Types (request/response structs, enums)oapi-codegen -generate types -package api -o internal/api/types.gen.go api/openapi.yaml# Fiber server interface + route registraroapi-codegen -generate fiber -package api -o internal/api/server.gen.go api/openapi.yaml# Embedded spec for /openapi endpointoapi-codegen -generate spec -package api -o internal/api/spec.gen.go api/openapi.yamlWired 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: objectrequired: [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 }. Usex-go-type-skip-optional-pointer: trueto keep generated structs tidy. -
Pagination: cursor-based by default —
?limit=50&cursor=abcreturning{ 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
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-codegenvalidator middleware (oapimiddleware.OapiRequestValidatorWithOptions). - Business rules (uniqueness, ownership) live in the service layer.
Mock server for parallel FE/BE work
prism mock api/openapi.yaml --port 4010The 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
| Check | Command |
|---|---|
| Spec lint | spectral lint api/openapi.yaml --ruleset .spectral.yaml |
| Spec valid | oapi-codegen -package api api/openapi.yaml > /dev/null |
| Generated code in sync | task oapi:generate && git diff --exit-code internal/api/*.gen.go |
| Backwards compat | oasdiff breaking api/openapi.yaml@HEAD~1 api/openapi.yaml@HEAD |