agentleFS
a-novel-kita-novel-kit/stack/.agents/skills/write-openapi/SKILL.md

write-openapi

Skill2 starsChanged 4 days agoMITView on GitHub

Write or review openapi.yaml for public REST APIs: endpoints, parameters, schemas, responses, examples, and contract compatibility. Internal gRPC contracts use write-proto.

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

25 rules. About 0 are common, 6 are distinctive, and 19 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 19

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Run these in order after any change to openapi.yaml:

    line 22
  2. Very specific:

    Never ship a change that fails pnpm lint:openapi.

    line 29
  3. Very specific:

    Deprecation process: add deprecated: true to the field or operation, add a description note explaining the replacement, then remove in the …

    line 108
  4. Very specific:

    Do not use nullable: true, a 3.0 extension removed in 3.1.

    line 121
  5. Very specific:

    Rule: prefer media-type level examples for full response/request bodies.

    line 162
  6. Very specific:

    Prefer const over single-value enum arrays in 3.1:

    line 167
  7. Very specific:

    Use enum when there are two or more values.

    line 177
  8. Very specific:

    Default is true (any extra properties allowed).

    line 181
  9. Very specific:

    Use additionalProperties: true for schemas that intentionally allow algorithm-specific extra fields (like jwk, which carries EdDSA x/crv, …

    line 183
  10. Very specific:

    Use additionalProperties: false for schemas where no extra fields should be present.

    line 185
  11. Distinctive:

    Never omit it for top-level response/request schemas — silence is ambiguous to code generators.

    line 186
  12. Very specific:

    Typed additionalProperties for dynamic-key objects: When a response is a map whose keys are dynamic but whose values all share a single …

    line 189
  13. Very specific:

    Use lowercase kebab-case for path segments.

    line 214
  14. Very specific:

    Use camelCase. Follows the pattern <entity><Operation> for resource operations:

    line 219
  15. Very specific:

    Use lowercase single-word tags to group operations in documentation: health, jwk.

    line 275
  16. Distinctive:

    Every element must be documented.

    line 282
  17. Very specific:

    Always use a YAML block scalar (|) for multi-line descriptions.

    line 298
  18. Very specific:

    Every responses status code in the spec must correspond to a reachable code path in the handler.

    line 310
  19. Distinctive:

    Every response schema field must correspond to a field the handler actually serializes.

    line 312
  20. Distinctive:

    Check the JSON tags on the Go struct.

    line 313
  21. Distinctive:

    The spec does not define handler behavior — it describes it: If the spec and the handler disagree, fix the spec (or the handler if it is …

    line 320
  22. Very specific:

    Use specific 4xx responses only for error conditions the handler explicitly checks and returns.

    line 366
  23. Very specific:

    Do not document a 4xx response unless the handler code has an explicit path that returns it.

    line 381
  24. Very specific:

    Every operation must have a default response.

    line 391
  25. Distinctive:

    Do not add spurious 4xx responses to silence these warnings — that would document behavior the server does not have.

    line 423

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

OpenAPI Specification Skill

openapi.yaml is the public contract for the REST API, consumed by documentation generators, client code generators, and API testing tools. Every field name, type, and status code is a durable commitment once published.

Before touching openapi.yaml, read the entire file, and read the Go handler code for every endpoint you are about to change. The spec must match exactly what the server returns, not what you think it should return.


After Every Edit

Run these in order after any change to openapi.yaml:

pnpm format         # runs Prettier over all files
pnpm lint:openapi   # validates the spec with Redocly

Never ship a change that fails pnpm lint:openapi. Warnings are not errors, but document any known, intentional one (see Suppressed Warnings).

Then update the TypeScript types in the JS client pkg/js/rest/src/ to match the spec change, and run pnpm lint:typecheck to confirm.


Project Layout

openapi.yaml         # The single-file OpenAPI 3.1 spec (edit this)
pkg/js/rest/src/     # JS client that must stay in sync with the spec

Everything lives in openapi.yaml; there is no multi-file splitting. Use $ref for reusable components defined under components/ in the same file:

$ref: "#/components/schemas/jwk"
$ref: "#/components/responses/notFound"
$ref: "#/components/parameters/jwkID"

Toolchain

Linting runs Redocly CLI via pnpm redocly lint openapi.yaml. A redocly.yaml at the project root extends the recommended ruleset with project-specific overrides; recommended is opinionated but not maximally strict, and some of its rules warn rather than error.

Rule overrides live in redocly.yaml:

extends: [recommended]
rules:
  operation-4xx-response: off # suppress for endpoints with no 4xx

Versioning

The spec version (info.version) must match package.json "version" and is updated automatically by the publish scripts. Never change info.version by hand — a manual edit diverges the YAML from package.json.


Breaking vs Non-Breaking Changes

The REST API is public, so any change that breaks existing callers needs a major version bump and coordination across consuming services.

Breaking — never do without a major version bump

ChangeWhy it breaks callers
Remove an endpoint (/jwk, /jwks, etc.)Callers get 404
Remove or rename a required response fieldCallers accessing the field get undefined/panic
Change a field's type (e.g., string → integer)Callers fail to deserialize
Change a 2xx status code (e.g., 200 → 201)Callers checking the code break
Add required: true to a previously optional parameterCallers omitting it now get 400
Rename an operationIdCode generators and clients using operationId break
Remove a named $ref component that is usedGenerated clients break

Non-breaking — always safe

ChangeNotes
Add a new endpointOld clients never call it
Add an optional response fieldOld clients ignore unknown fields
Add a new optional query parameterOld callers don't send it; server still works
Add a new error status code (4xx, 5xx)Old callers already handle unknown errors
Change a description or exampleNo runtime impact
Add a new $ref componentUnused until referenced
Mark a field deprecated: trueSignals intent without breaking callers

Deprecation process: add deprecated: true to the field or operation, add a description note explaining the replacement, then remove in the next major version. Never remove a deprecated item in the same release it was deprecated.


OpenAPI 3.1 Specifics

This project uses OpenAPI 3.1, which aligns fully with JSON Schema Draft 2020-12. Key differences from 3.0 that affect writing this spec:

Nullable fields

Do not use nullable: true, a 3.0 extension removed in 3.1. Use type unions instead:

# WRONG (3.0 style):
type: string
nullable: true

# CORRECT (3.1):
type: [string, "null"]

Examples

Three placement options exist, from most to least specific:

  1. Media-type level (content.{media}.examples) — named examples object, the most flexible; Redocly renders all of them. Use for multiple named examples on an operation response.

    content:
      application/json:
        examples:
          active:
            value: { "status": "up" }
          degraded:
            value: { "status": "down", "err": "connection refused" }
  2. Schema level (schema.examples) — JSON Schema array; only the first item is typically rendered. Use for property-level inline examples, or when one example is enough.

    schema:
      type: string
      examples: [auth, auth-refresh]
  3. Schema example (singular, 3.0 compat) — single value, lower priority than the array form. Avoid in new code; use examples instead.

Rule: prefer media-type level examples for full response/request bodies. Use schema-level examples (array) for individual parameters and schema properties.

const and enum

Prefer const over single-value enum arrays in 3.1:

# WRONG (verbose):
enum: [sig]

# CORRECT (3.1):
const: sig

Use enum when there are two or more values.

additionalProperties

Default is true (any extra properties allowed). Be explicit:

  • Use additionalProperties: true for schemas that intentionally allow algorithm-specific extra fields (like jwk, which carries EdDSA x/crv, RSA n/e, etc.).
  • Use additionalProperties: false for schemas where no extra fields should be present.
  • Never omit it for top-level response/request schemas — silence is ambiguous to code generators.

Typed additionalProperties for dynamic-key objects. When a response is a map whose keys are dynamic but whose values all share a single shape, use a $ref (or inline schema) as the value of additionalProperties. Generators then know the value type without every key listed:

health:
  type: object
  required: ["client:postgres"]
  additionalProperties:
    $ref: "#/components/schemas/healthStatus" # every key maps to this type
  properties:
    "client:postgres": # explicitly document known keys
      $ref: "#/components/schemas/healthStatus"

The properties block calls out the key that must always be present (matched by required); additionalProperties covers future keys added without a spec change. On a map with typed values, additionalProperties: true reads as any to generators and loses type safety.


Naming Conventions

Paths

Use lowercase kebab-case for path segments. This project uses flat paths (/jwk, /jwks, /ping, /healthcheck) — follow this pattern for new endpoints.

operationId

Use camelCase. Follows the pattern <entity><Operation> for resource operations:

OperationoperationId
List JWKsjwkList
Get one JWKjwkGet
Pingping
Healthcheckhealthcheck

operationId values must be unique across the entire spec. Code generators use them to name functions, so treat them like function names. Renaming one is breaking; add a new operation instead if a rename is truly needed.

Schema names

camelCase for reusable schemas under components/schemas, matching the $ref keys the fleet's specs already use:

NameUse
jwkA public JSON Web Key
jwkIDThe UUID identifier of a key
healthStatusStatus of a single dependency

An initialism keeps its casing after the first segment (jwkID, not jwkId).

Parameter names

camelCase for the $ref key under components/parameters; snake_case for the actual name in the query string (to match what the Go handler reads with r.URL.Query().Get("id")):

components:
  parameters:
    jwkID: # camelCase $ref key
      name: id # snake_case query param name
      in: query

Response names

camelCase:

components:
  responses:
    jwkList:
    jwkGet:
    notFound:
    badRequest:
    internalError:

Tags

Use lowercase single-word tags to group operations in documentation: health, jwk. Define tags at the top level under tags: with descriptions if more than two exist.


Documentation Requirements

Every element must be documented. No exceptions.

ElementRequired
info.descriptionMulti-line markdown; explain the service's purpose and scope
Each path operationsummary (one line) + description (markdown, explains behavior and edge cases)
Each parameterdescription explaining what the value controls
Each responsedescription — one sentence is sufficient for error responses
Each schemadescription
Each schema propertydescription
Each named $ref componentDocumented inside the component definition

Summary vs description:

  • summary — one sentence, no markdown, shown in navigation/tooling.
  • description — markdown allowed; carries nuance, caveats, and edge-case behavior. Always use a YAML block scalar (|) for multi-line descriptions.

Examples — every schema property with a known range of values must include examples. Parameters that accept a finite set of identifiers (like usage) must show real values from the server config, not placeholders.


Alignment with the Go Handler Layer

The spec must exactly mirror what the Go handlers return. Rules:

  • Every responses status code in the spec must correspond to a reachable code path in the handler. If the handler can never return 422, do not document 422.
  • Every response schema field must correspond to a field the handler actually serializes. Check the JSON tags on the Go struct.
  • Parameter required: true must match whether the handler actually validates and rejects missing inputs with 400.
  • The usage parameter values documented as examples must match the registered usages in internal/config/jwks.config.yaml (auth, auth-refresh). Those are named signing configuration identifiers, not JWK use values (sig, enc).

The spec does not define handler behavior — it describes it. If the spec and the handler disagree, fix the spec (or the handler if it is wrong), but never let them drift.


Response Design

Status codes

Pick each code by its RFC 9110 meaning and send the headers that meaning requires. Handlers, platform routes and specs all follow this table:

OutcomeCodeRequired headers
A read, or an action that returns a body200
Created an addressable resource201Location naming it
Accepted work that finishes later202Location of a status resource
Succeeded with nothing to return204no body
Conditional GET on an unchanged resource304the ETag and Cache-Control of 200
Redirect after a POST (POST/redirect/GET)303Location
Moved, temporarily or for good, method kept307 / 308Location
Missing or invalid credentials401WWW-Authenticate
Authenticated but not allowed403
Method not served on this path405Allow
Write conflicts with the current state409
If-Match or If-Unmodified-Since failed412
Rate limited429Retry-After
  • Never 301 or 302. A client may turn a redirected POST into a GET on either, so the method depends on the client. 303, 307 and 308 state it. A redirect answering a request that carried credentials uses 303, never 307, which reposts them (RFC 9700 §4.12).
  • Another user's resource is 404. Answering 403 confirms the resource exists.
  • Cache what is public and stable. A public read that changes rarely, such as a JWKS, sends Cache-Control and an ETag, and answers If-None-Match with 304. http.ServeContent does the conditional part once the ETag header is set.
  • 1xx comes from the stack. Go's server sends 100 Continue itself when a handler first reads a body sent with Expect: 100-continue; never write it by hand. 103 Early Hints suits an HTML page whose critical assets are known before its body; confirm the load balancer forwards it first.
  • A changed 2xx is breaking. Fixing a published route's success code waits for a planned major (see Breaking vs Non-Breaking Changes). Adding a required header such as WWW-Authenticate or Allow is not.

Error responses

Use specific 4xx responses only for error conditions the handler explicitly checks and returns; everything else falls to the default response.

responses:
  "200":
    $ref: "#/components/responses/jwkGet"
  "400":
    $ref: "#/components/responses/badRequest" # only if the handler validates input
  "404":
    $ref: "#/components/responses/notFound" # only if the handler returns 404
  default:
    $ref: "#/components/responses/internalError"

Do not document a 4xx response unless the handler code has an explicit path that returns it. A 400 from /jwk (invalid UUID) is a real handler path; a 400 from /jwks does not exist (see Suppressed Warnings).

A rate limit is such a path. Every limited route references one tooManyRequests component, 429 with an integer Retry-After header; only the unlimited liveness ping omits it. A route with a strict limit states its numbers in its description, so clients can pace themselves.

The default response

Every operation must have a default response. It catches any status code not explicitly listed: internal errors and unexpected conditions.

Reuse response components

Define all responses under components/responses and reference them; never inline a response schema:

responses:
  "404":
    $ref: "#/components/responses/notFound" # NOT inline

Suppressed Warnings

Three operations intentionally have no 4xx responses. Without operation-4xx-response: off in redocly.yaml, Redocly would flag them with:

openapi.yaml: Operation must have at least one `4XX` response. [operation-4xx-response]
Affected: /ping GET, /healthcheck GET, /jwks GET

None of these handlers returns a 4xx, by design:

  • /ping — always returns 200 (liveness check only).
  • /healthcheck — always returns 200 regardless of dependency status (callers read the body to assess health).
  • /jwks — accepts any string for usage and returns an empty list for an unrecognized usage; it never validates input with a 400.

Do not add spurious 4xx responses to silence these warnings — that would document behavior the server does not have.

The suppression in the root redocly.yaml:

extends: [recommended]
rules:
  operation-4xx-response: off

Common Pitfalls

Each of these is covered above; run the list as a checklist before shipping an edit.

  • nullable: true instead of a 3.1 type union
  • usage examples taken from JWK use values instead of internal/config/jwks.config.yaml
  • A 4xx documented on a path the handler never returns
  • An inline response schema instead of a components/responses $ref
  • A renamed operationId
  • 3.0 singular example on a new schema
  • pkg/js/rest/src/ types left stale after a response-schema change
  • A new required parameter on an existing endpoint
  • A hand-edited info.version
  • Single-item enum where const fits
  • additionalProperties omitted on a closed schema

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.