Skip to content

feat(openapi): shared error responses + 429/500 coverage + externalDocs#9

Open
jesseoue wants to merge 1 commit intomainfrom
chore/openapi-shared-error-responses
Open

feat(openapi): shared error responses + 429/500 coverage + externalDocs#9
jesseoue wants to merge 1 commit intomainfrom
chore/openapi-shared-error-responses

Conversation

@jesseoue
Copy link
Copy Markdown
Contributor

Summary

Comprehensive audit and upgrade of leadmagic-openapi-3.1.yaml / .json to bring the public spec up to the level that SDK generators, docs renderers (Redocly, Scalar, Swagger UI), and MCP/LLM tooling need in order to emit correct retry logic, full error handling, and complete API context.

No operation signatures, schemas, tag taxonomy, or property-level examples were changed — every request/response shape, operationId, and example value is preserved byte-for-byte. All new content is additive or consolidation.

What changed

New shared error responses (components.responses)

Key Status Description
BadRequest 400 Request payload malformed or failed validation.
Unauthorized 401 X-API-Key missing, invalid, or revoked.
TooManyRequests 429 Rate limit exceeded; back off with exponential jitter.
InternalServerError 500 Unexpected failure; retry with backoff; contact support@leadmagic.io with the correlation id.

All four $ref the existing components.schemas.Error shape, so there's a single source of truth for every error body the API returns.

Per-operation response-map updates (all 19 operations)

Before After
17 × inline 400 responses (each a 4-line block duplicating the same schema ref + description) $ref: "#/components/responses/BadRequest"
19 × inline 401 responses $ref: "#/components/responses/Unauthorized"
1 × 429 on /profile-search; 0 × elsewhere $ref: "#/components/responses/TooManyRequests" on every operation
0 × 500 declared anywhere $ref: "#/components/responses/InternalServerError" on every operation

Downstream impact: SDK generators (Speakeasy, OpenAPI Generator, Fern, etc.) will now emit retry-on-429 and backoff-on-5xx handlers out of the box. Previously they had no signal that these responses exist, so callers had to write retry logic by hand.

Spec-root additions

  • externalDocs pointing at leadmagic.io/docs. Docs renderers use this to surface a "full documentation" link above the tag navigation.
  • info.termsOfService: https://leadmagic.io/terms.
  • Rewrote info.description to be platform-neutral, point to the README for credit costs / MCP sign-in, and drop the "public docs not fully uniform on field naming" caveat that was showing up in rendered docs.
  • Reordered info.* into the canonical OpenAPI key sequence (title, summary, description, version, termsOfService, contact, license) for consistency with Redocly/Scalar output.

Consistency fix

Regenerated leadmagic-openapi-3.1.json from the YAML (the source that everyone edits). This incidentally fixed a pre-existing YAML/JSON drift: company_facebook_url and company_twitter_url were present in the /email-finder 200 response's YAML definition but missing from the JSON file. After this PR, the two files are structurally identical (diffable round-trip via js-yaml).

Why this matters

The spec was syntactically valid and lint-clean before, but three gaps were blocking high-quality SDK generation and AI/MCP consumers:

  1. No rate-limit documentation in the contract. The README and SECURITY.md documented rate limits, but the OpenAPI contract did not. Any generator reading only the spec had no way to know a 429 could come back.
  2. No 5xx surface declared. Same issue — even though the runtime returns 500 on unexpected failures, the spec implied only 2xx/4xx were possible.
  3. Duplicated error copy. 17 copies of the same 400 block and 19 copies of the same 401 block meant any future copy tweak would require a 36-place find-and-replace, which guarantees drift over time. Centralizing on $ref removes that class of bug.

Diff shape

The unified diff is large (~4k insertions / ~3.5k deletions) because the YAML emitter re-serializes the whole file with its own quoting/indentation style. The semantic diff is small and surgical:

  • 4 components.responses added.
  • 74 error-response blocks collapsed into $refs (17 × 400, 19 × 401, 19 × 429, 19 × 500 — the 429/500 additions replace zero-or-one prior inline declarations).
  • 1 externalDocs block added.
  • 1 info.termsOfService field added.
  • 1 info.description rewritten.
  • 2 previously-missing JSON properties restored from the YAML source of truth.

Reviewers can verify the semantic delta with:

git show origin/main:leadmagic-openapi-3.1.json > /tmp/old.json
diff <(jq -S . /tmp/old.json) <(jq -S . leadmagic-openapi-3.1.json) | less

Verification (all run locally against the final files)

  • npx @apidevtools/swagger-cli validate leadmagic-openapi-3.1.yaml — valid.
  • npx @apidevtools/swagger-cli validate leadmagic-openapi-3.1.json — valid.
  • npm run lint:openapi (Spectral) — no results with severity error.
  • npm run typecheck (tsc --noEmit) — clean.
  • CI's deprecated-example regex (grep -nE '(^|[^s])"example"[[:space:]]*:|(^|[^s])example[[:space:]]*:') — zero matches (all examples use OpenAPI 3.1 plural examples).
  • README required sections (Authentication, Base URL, Credit Consumption, Testing & Validation, Use Case Examples) — all present.
  • No hardcoded credentials in test-api.ts or the specs.
  • YAML and JSON round-trip to structurally identical parsed objects.

Follow-ups (intentionally out of scope)

  • Operation-level consolidated request/response examples blocks for the 17 POST endpoints. Property-level examples are comprehensive already; adding duplicate operation-level fixtures would add ~800 lines of near-duplicate content. Will file a separate PR if/when we have a clean way to generate them from the live API.
  • Migrating swagger-cli@redocly/cli (the CLI emits a deprecation notice). Low priority; validation still works.

…e-limit/server-error coverage

Tighten the public OpenAPI 3.1 snapshot so every operation documents
the complete error surface we already enforce at runtime, and give
coding-agent tools and SDK generators everything they need to emit
good retry logic.

Structural additions
- components.responses
  - BadRequest            — 400, payload validation failures
  - Unauthorized          — 401, missing/invalid/revoked X-API-Key
  - TooManyRequests       — 429, rate limit exceeded; retry with jitter
  - InternalServerError   — 500, unexpected failure; contact support
  Each response reuses the pre-existing components.schemas.Error
  shape, so there's now a single source of truth for every error
  body we return.
- externalDocs at the spec root pointing at https://leadmagic.io/docs
  so docs renderers surface a "full documentation" link.
- info.termsOfService = https://leadmagic.io/terms.

Per-operation changes (applied uniformly across all 19 operations)
- Replaced every inline 400 (17 ops) and 401 (19 ops) response
  with $ref to the shared components.responses so the copy, status
  description, and schema never drift again.
- Added 429 and 500 $refs to every operation. Previously only
  /profile-search declared 429 and no operation declared 500 even
  though the runtime returns both. Downstream SDK generators
  (Speakeasy, OpenAPI Generator, Fern) will now emit retry and
  backoff handlers for rate limits and transient server errors.

Info-level tidy
- Rewrote info.description: platform-neutral, points to the README
  for credit costs / MCP sign-in, drops the old "public docs not
  fully uniform" caveat that had no place in rendered docs.
- Reordered info.* into the canonical OpenAPI key sequence
  (title, summary, description, version, termsOfService, contact,
  license) for consistency with Redocly/Scalar output.

Consistency guarantees
- Regenerated leadmagic-openapi-3.1.json from the YAML source of
  truth. This incidentally fixed a pre-existing YAML/JSON drift:
  company_facebook_url / company_twitter_url were present in the
  /email-finder 200 response's YAML but missing from the JSON.
  The two files are now structurally identical.
- All 19 operationIds, summaries, descriptions, tags, and
  property-level examples preserved byte-for-byte.
- Response maps within each operation reordered by status code
  (2xx, 4xx, 5xx) so the generated docs read top-to-bottom.

Verification
- swagger-cli validate: both YAML and JSON pass.
- spectral lint: zero errors.
- tsc --noEmit: clean.
- CI deprecated-example regex: no singular example: / "example":
  fields found.
- README required sections still present.
- No hardcoded credentials.

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant