OpenAPI as the source of truth
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/backend/references/openapi.md |
| Description | Not specified |
Source Content
OpenAPI as the source of truth
The spec at api/openapi.yaml is authoritative. Code is generated from it, never the reverse. You implement a generated interface; you never hand-write the route signatures the spec already describes. Contract design is api-designer’s job — this file governs the generate-and-implement loop.
Generated code is committed
The output of oapi-codegen is checked into git. This is deliberate:
- Reviewable diffs — a contract change shows up in the PR as a concrete code change, not a build-time surprise.
- Hermetic CI — builds do not depend on regenerating code, only on verifying it is current.
- No drift — the committed code is the single artifact everyone compiles against.
oapi-codegen config
A oapi-codegen.yaml config keeps generation reproducible. Generate the Fiber server interface, the types, and the spec embed.
package: apigenerate: fiber-server: true models: true embedded-spec: trueoutput: internal/api/api.gen.goDrive it from go generate so the command lives next to the code:
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config api/oapi-codegen.yaml api/openapi.yamlImplement the generated interface
oapi-codegen emits a ServerInterface with one method per operationId. Your handler struct implements it; the compiler guarantees every operation in the spec has an implementation.
package handler
import "github.com/acme/billing/internal/api"
// Server implements the generated api.ServerInterface.type Server struct { invoices *service.InvoiceService}
// compile-time assertion: Server satisfies the generated contractvar _ api.ServerInterface = (*Server)(nil)
// FinalizeInvoice implements the operationId from openapi.yaml.func (s *Server) FinalizeInvoice(c *fiber.Ctx, id string) error { inv, err := s.invoices.Finalize(c.Context(), domain.InvoiceID(id)) // ... map domain error -> status (see references/errors.md) return c.Status(fiber.StatusOK).JSON(toResponse(inv))}The var _ api.ServerInterface = (*Server)(nil) line is the safety net: add an operation to the spec, regenerate, and the build breaks until you implement it.
Regeneration is a build step
task openapi regenerates from the spec:
go generate ./...The drift check
CI regenerates and fails if the committed code no longer matches the spec — this is what makes “spec is the source of truth” enforceable rather than aspirational.
go generate ./...git diff --exit-code -- internal/api/api.gen.goIf the diff is non-empty, someone edited the generated file by hand or changed the spec without regenerating — the build fails. Add this as a CI step and as a task openapi-check target.
Redocly upgrade path (Wave 3, not yet installed)
scripts/lint_openapi.sh’s RFC 9457 check is a text-proximity scan today — it looks for title/status/detail/type keys within a line window, not a real schema parse. redocly lint would replace that with an actual OpenAPI/JSON Schema validation pass.
redocly is not installed on this machine (command -v redocly finds nothing). The upgrade follows the same graceful-degradation pattern as every other tool in this skill: lints.toml carries a commented-out [[lint]] block for it, and scripts/lint.py would skip it with a warning — never a hard failure — until it’s installed. Install with npm i -g @redocly/cli, then uncomment the block in lints.toml.
Where the boundary types live
The generated models are the transport representation, not the domain. The handler maps between generated request/response types and domain types — the same boundary where any/interface{} is allowed (it is a protocol edge). The service and domain never import the generated api package.
client JSON <-> api.* (generated) <-> [handler maps] <-> domain.* <-> service / repository