mirror of
https://github.com/duthaho/claudekit.git
synced 2026-07-24 21:51:02 +03:00
838 lines
21 KiB
Markdown
838 lines
21 KiB
Markdown
---
|
|
name: openapi
|
|
description: >
|
|
Use this skill when designing, documenting, or generating REST API specifications using OpenAPI/Swagger. Trigger on keywords like OpenAPI, Swagger, API spec, REST documentation, API schema, request body, response schema, and API client generation. Also apply when adopting design-first API development, validating API contracts, or setting up auto-generated API documentation for FastAPI, Express, or NestJS endpoints.
|
|
---
|
|
|
|
# OpenAPI & REST API Design
|
|
|
|
## When to Use
|
|
|
|
- Documenting REST APIs
|
|
- Generating API clients
|
|
- API design-first development
|
|
- Defining webhook contracts
|
|
- Establishing pagination, versioning, or auth patterns for a new service
|
|
|
|
## When NOT to Use
|
|
|
|
- Internal-only scripts or automation that do not expose HTTP endpoints
|
|
- CLI tools and command-line utilities without a REST interface
|
|
- GraphQL APIs where a different specification format applies
|
|
|
|
---
|
|
|
|
## Core Patterns
|
|
|
|
### 1. OpenAPI 3.1 Specification Structure
|
|
|
|
A complete spec skeleton showing every top-level section. Use `$ref` to split
|
|
large specs into per-resource files.
|
|
|
|
```yaml
|
|
openapi: 3.1.0
|
|
info:
|
|
title: Acme API
|
|
version: 2.0.0
|
|
description: Public API for the Acme platform.
|
|
contact:
|
|
name: API Support
|
|
email: api@acme.dev
|
|
license:
|
|
name: MIT
|
|
url: https://opensource.org/licenses/MIT
|
|
|
|
servers:
|
|
- url: https://api.acme.dev/v2
|
|
description: Production
|
|
- url: https://staging-api.acme.dev/v2
|
|
description: Staging
|
|
|
|
tags:
|
|
- name: Users
|
|
description: User management operations
|
|
- name: Orders
|
|
description: Order lifecycle operations
|
|
|
|
paths:
|
|
/users:
|
|
$ref: './paths/users.yaml'
|
|
/users/{userId}:
|
|
$ref: './paths/users-by-id.yaml'
|
|
/orders:
|
|
$ref: './paths/orders.yaml'
|
|
|
|
components:
|
|
schemas:
|
|
$ref: './components/schemas/_index.yaml'
|
|
securitySchemes:
|
|
BearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
ApiKeyAuth:
|
|
type: apiKey
|
|
in: header
|
|
name: X-API-Key
|
|
|
|
security:
|
|
- BearerAuth: []
|
|
|
|
webhooks:
|
|
orderCompleted:
|
|
$ref: './webhooks/order-completed.yaml'
|
|
```
|
|
|
|
**Organizing with `$ref`** -- keep one file per resource under `paths/` and
|
|
shared schemas under `components/schemas/`. A bundler such as
|
|
`@redocly/cli bundle` resolves references into a single file for tooling.
|
|
|
|
```
|
|
spec/
|
|
├── openapi.yaml # Root document
|
|
├── paths/
|
|
│ ├── users.yaml
|
|
│ ├── users-by-id.yaml
|
|
│ └── orders.yaml
|
|
├── components/
|
|
│ └── schemas/
|
|
│ ├── _index.yaml
|
|
│ ├── User.yaml
|
|
│ ├── Order.yaml
|
|
│ └── ProblemDetail.yaml
|
|
└── webhooks/
|
|
└── order-completed.yaml
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Path & Operation Patterns
|
|
|
|
#### RESTful URL Naming Conventions
|
|
|
|
- Use **plural nouns** for collections: `/users`, `/orders`.
|
|
- Use **path parameters** for single-resource access: `/users/{userId}`.
|
|
- Nest only one level deep: `/users/{userId}/orders` (not deeper).
|
|
- Use **query parameters** for filtering, sorting, and pagination.
|
|
- Avoid verbs in paths -- let HTTP methods convey the action.
|
|
|
|
#### CRUD Operations
|
|
|
|
```yaml
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
tags: [Users]
|
|
summary: List users
|
|
parameters:
|
|
- $ref: '#/components/parameters/PageCursor'
|
|
- $ref: '#/components/parameters/PageSize'
|
|
- name: status
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [active, inactive]
|
|
responses:
|
|
'200':
|
|
description: Paginated list of users
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UserListResponse'
|
|
|
|
post:
|
|
operationId: createUser
|
|
tags: [Users]
|
|
summary: Create a user
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateUserRequest'
|
|
responses:
|
|
'201':
|
|
description: User created
|
|
headers:
|
|
Location:
|
|
schema:
|
|
type: string
|
|
description: URL of the new resource
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/User'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
|
|
/users/{userId}:
|
|
parameters:
|
|
- name: userId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
|
|
get:
|
|
operationId: getUser
|
|
tags: [Users]
|
|
summary: Get a single user
|
|
responses:
|
|
'200':
|
|
description: User found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/User'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
|
|
patch:
|
|
operationId: updateUser
|
|
tags: [Users]
|
|
summary: Partially update a user
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdateUserRequest'
|
|
responses:
|
|
'200':
|
|
description: User updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/User'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
|
|
delete:
|
|
operationId: deleteUser
|
|
tags: [Users]
|
|
summary: Delete a user
|
|
responses:
|
|
'204':
|
|
description: User deleted
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
```
|
|
|
|
#### Path Parameters vs Query Parameters
|
|
|
|
| Use case | Mechanism | Example |
|
|
|----------|-----------|---------|
|
|
| Identify a specific resource | Path parameter | `/orders/{orderId}` |
|
|
| Filter a collection | Query parameter | `/orders?status=shipped` |
|
|
| Sort a collection | Query parameter | `/orders?sort=-createdAt` |
|
|
| Paginate | Query parameter | `/orders?cursor=abc&limit=20` |
|
|
| Expand nested data | Query parameter | `/orders?expand=items,customer` |
|
|
|
|
---
|
|
|
|
### 3. Request Body Patterns
|
|
|
|
#### JSON Request Body with Validation
|
|
|
|
```yaml
|
|
components:
|
|
schemas:
|
|
CreateUserRequest:
|
|
type: object
|
|
required:
|
|
- email
|
|
- name
|
|
properties:
|
|
email:
|
|
type: string
|
|
format: email
|
|
maxLength: 254
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
role:
|
|
type: string
|
|
enum: [admin, member, viewer]
|
|
default: member
|
|
additionalProperties: false
|
|
```
|
|
|
|
Implementation in **FastAPI** (Python):
|
|
|
|
```python
|
|
from pydantic import BaseModel, EmailStr, Field
|
|
|
|
class CreateUserRequest(BaseModel):
|
|
email: EmailStr
|
|
name: str = Field(min_length=1, max_length=100)
|
|
role: str = Field(default="member", pattern="^(admin|member|viewer)$")
|
|
|
|
model_config = {"extra": "forbid"}
|
|
```
|
|
|
|
Implementation in **Express** (TypeScript with Zod):
|
|
|
|
```typescript
|
|
import { z } from "zod";
|
|
|
|
const CreateUserRequest = z.object({
|
|
email: z.string().email().max(254),
|
|
name: z.string().min(1).max(100),
|
|
role: z.enum(["admin", "member", "viewer"]).default("member"),
|
|
}).strict();
|
|
|
|
type CreateUserRequest = z.infer<typeof CreateUserRequest>;
|
|
```
|
|
|
|
#### Multipart Form Data (File Uploads)
|
|
|
|
```yaml
|
|
/users/{userId}/avatar:
|
|
put:
|
|
operationId: uploadAvatar
|
|
tags: [Users]
|
|
summary: Upload user avatar
|
|
parameters:
|
|
- name: userId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- file
|
|
properties:
|
|
file:
|
|
type: string
|
|
format: binary
|
|
description: Image file (JPEG or PNG, max 5 MB)
|
|
caption:
|
|
type: string
|
|
maxLength: 200
|
|
encoding:
|
|
file:
|
|
contentType: image/jpeg, image/png
|
|
responses:
|
|
'200':
|
|
description: Avatar updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
url:
|
|
type: string
|
|
format: uri
|
|
'413':
|
|
$ref: '#/components/responses/PayloadTooLarge'
|
|
```
|
|
|
|
#### Content Negotiation
|
|
|
|
Support multiple response formats by listing them under `content`:
|
|
|
|
```yaml
|
|
responses:
|
|
'200':
|
|
description: Export data
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ExportData'
|
|
text/csv:
|
|
schema:
|
|
type: string
|
|
application/pdf:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
```
|
|
|
|
Clients select a format with the `Accept` header. Document which formats your
|
|
API actually supports so consumers do not have to guess.
|
|
|
|
---
|
|
|
|
### 4. Response Patterns
|
|
|
|
#### Success Responses
|
|
|
|
| Code | Meaning | Typical use |
|
|
|------|---------|-------------|
|
|
| `200` | OK | GET, PATCH, general success |
|
|
| `201` | Created | POST that creates a resource |
|
|
| `202` | Accepted | Async operation started |
|
|
| `204` | No Content | DELETE, or PUT with no body returned |
|
|
|
|
Always return a `Location` header with `201` pointing to the new resource.
|
|
|
|
#### Error Responses -- RFC 7807 Problem Details
|
|
|
|
Define a single reusable error schema based on RFC 7807:
|
|
|
|
```yaml
|
|
components:
|
|
schemas:
|
|
ProblemDetail:
|
|
type: object
|
|
required:
|
|
- type
|
|
- title
|
|
- status
|
|
properties:
|
|
type:
|
|
type: string
|
|
format: uri
|
|
description: URI reference identifying the problem type.
|
|
example: https://api.acme.dev/problems/validation-error
|
|
title:
|
|
type: string
|
|
description: Short human-readable summary.
|
|
example: Validation Error
|
|
status:
|
|
type: integer
|
|
description: HTTP status code.
|
|
example: 422
|
|
detail:
|
|
type: string
|
|
description: Human-readable explanation specific to this occurrence.
|
|
example: "Field 'email' must be a valid email address."
|
|
instance:
|
|
type: string
|
|
format: uri
|
|
description: URI identifying this specific occurrence.
|
|
errors:
|
|
type: array
|
|
description: Field-level validation errors (optional extension).
|
|
items:
|
|
type: object
|
|
properties:
|
|
field:
|
|
type: string
|
|
example: email
|
|
message:
|
|
type: string
|
|
example: Must be a valid email address.
|
|
code:
|
|
type: string
|
|
example: invalid_format
|
|
|
|
responses:
|
|
NotFound:
|
|
description: Resource not found
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/ProblemDetail'
|
|
example:
|
|
type: https://api.acme.dev/problems/not-found
|
|
title: Not Found
|
|
status: 404
|
|
detail: User with ID '550e8400' was not found.
|
|
|
|
ValidationError:
|
|
description: Request validation failed
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/ProblemDetail'
|
|
example:
|
|
type: https://api.acme.dev/problems/validation-error
|
|
title: Validation Error
|
|
status: 422
|
|
errors:
|
|
- field: email
|
|
message: Must be a valid email address.
|
|
code: invalid_format
|
|
|
|
Conflict:
|
|
description: Resource conflict
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/ProblemDetail'
|
|
|
|
PayloadTooLarge:
|
|
description: Request payload exceeds limit
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/ProblemDetail'
|
|
```
|
|
|
|
Use the `application/problem+json` media type for all error responses to signal
|
|
RFC 7807 compliance.
|
|
|
|
---
|
|
|
|
### 5. Authentication Schemes
|
|
|
|
#### Bearer Token (JWT)
|
|
|
|
```yaml
|
|
components:
|
|
securitySchemes:
|
|
BearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
|
|
security:
|
|
- BearerAuth: []
|
|
```
|
|
|
|
Override per-operation to allow unauthenticated access:
|
|
|
|
```yaml
|
|
paths:
|
|
/health:
|
|
get:
|
|
security: [] # No auth required
|
|
responses:
|
|
'200':
|
|
description: Healthy
|
|
```
|
|
|
|
#### API Key
|
|
|
|
```yaml
|
|
components:
|
|
securitySchemes:
|
|
ApiKeyHeader:
|
|
type: apiKey
|
|
in: header
|
|
name: X-API-Key
|
|
ApiKeyQuery:
|
|
type: apiKey
|
|
in: query
|
|
name: api_key
|
|
```
|
|
|
|
#### OAuth2 Flows
|
|
|
|
```yaml
|
|
components:
|
|
securitySchemes:
|
|
OAuth2:
|
|
type: oauth2
|
|
flows:
|
|
authorizationCode:
|
|
authorizationUrl: https://auth.acme.dev/authorize
|
|
tokenUrl: https://auth.acme.dev/token
|
|
refreshUrl: https://auth.acme.dev/token
|
|
scopes:
|
|
users:read: Read user profiles
|
|
users:write: Create and update users
|
|
orders:read: Read orders
|
|
|
|
paths:
|
|
/users:
|
|
get:
|
|
security:
|
|
- OAuth2: [users:read]
|
|
```
|
|
|
|
---
|
|
|
|
### 6. Pagination Patterns
|
|
|
|
#### Cursor-Based Pagination (Recommended)
|
|
|
|
Best for large, real-time datasets where rows may be inserted or deleted
|
|
between pages.
|
|
|
|
```yaml
|
|
components:
|
|
parameters:
|
|
PageCursor:
|
|
name: cursor
|
|
in: query
|
|
description: Opaque cursor returned by a previous response.
|
|
schema:
|
|
type: string
|
|
PageSize:
|
|
name: limit
|
|
in: query
|
|
description: Maximum items per page.
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 100
|
|
default: 20
|
|
|
|
schemas:
|
|
UserListResponse:
|
|
type: object
|
|
required:
|
|
- data
|
|
- pagination
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/User'
|
|
pagination:
|
|
type: object
|
|
required:
|
|
- hasMore
|
|
properties:
|
|
nextCursor:
|
|
type: string
|
|
nullable: true
|
|
hasMore:
|
|
type: boolean
|
|
```
|
|
|
|
#### Offset-Based Pagination
|
|
|
|
Simpler but less efficient for large tables and susceptible to drift when data
|
|
changes between requests.
|
|
|
|
```yaml
|
|
components:
|
|
parameters:
|
|
PageOffset:
|
|
name: offset
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
default: 0
|
|
PageLimit:
|
|
name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 100
|
|
default: 20
|
|
|
|
schemas:
|
|
PaginatedResponse:
|
|
type: object
|
|
required:
|
|
- data
|
|
- total
|
|
- offset
|
|
- limit
|
|
properties:
|
|
data:
|
|
type: array
|
|
items: {}
|
|
total:
|
|
type: integer
|
|
description: Total number of matching records.
|
|
offset:
|
|
type: integer
|
|
limit:
|
|
type: integer
|
|
```
|
|
|
|
#### Response Envelope Pattern
|
|
|
|
Wrap every collection in a consistent envelope so clients always know where to
|
|
find the data and metadata:
|
|
|
|
```json
|
|
{
|
|
"data": [ ... ],
|
|
"pagination": { "nextCursor": "abc123", "hasMore": true },
|
|
"meta": { "requestId": "req_xyz", "timestamp": "2026-03-29T12:00:00Z" }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 7. API Versioning
|
|
|
|
#### URL Versioning
|
|
|
|
```yaml
|
|
servers:
|
|
- url: https://api.acme.dev/v1
|
|
description: Version 1 (deprecated)
|
|
- url: https://api.acme.dev/v2
|
|
description: Version 2 (current)
|
|
```
|
|
|
|
Pros: explicit, easy to route, cache-friendly.
|
|
Cons: duplicates paths across versions, harder to share schemas.
|
|
|
|
#### Header Versioning
|
|
|
|
```yaml
|
|
parameters:
|
|
- name: X-API-Version
|
|
in: header
|
|
required: false
|
|
schema:
|
|
type: string
|
|
enum: ['2024-01-15', '2025-06-01']
|
|
default: '2025-06-01'
|
|
description: Date-based API version. Defaults to latest stable.
|
|
```
|
|
|
|
Pros: clean URLs, fine-grained control.
|
|
Cons: less discoverable, harder to test in a browser.
|
|
|
|
#### Trade-offs Summary
|
|
|
|
| Approach | Discoverability | URL cleanliness | Caching | Migration effort |
|
|
|----------|----------------|-----------------|---------|-----------------|
|
|
| URL path | High | Lower | Easy | Higher (path changes) |
|
|
| Header | Lower | High | Needs Vary header | Lower |
|
|
| Query param | Medium | Medium | Easy | Lower |
|
|
|
|
Pick one approach and use it consistently. URL versioning is the most common
|
|
choice for public APIs; header versioning suits internal services.
|
|
|
|
---
|
|
|
|
### 8. Webhook Specifications
|
|
|
|
OpenAPI 3.1 supports a top-level `webhooks` key for documenting outbound
|
|
event payloads your API will send to consumer-registered URLs.
|
|
|
|
```yaml
|
|
webhooks:
|
|
orderCompleted:
|
|
post:
|
|
operationId: onOrderCompleted
|
|
summary: Fired when an order reaches "completed" status.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/OrderCompletedEvent'
|
|
responses:
|
|
'200':
|
|
description: Webhook received successfully.
|
|
|
|
components:
|
|
schemas:
|
|
WebhookEventBase:
|
|
type: object
|
|
required:
|
|
- id
|
|
- type
|
|
- createdAt
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
type:
|
|
type: string
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
|
|
OrderCompletedEvent:
|
|
allOf:
|
|
- $ref: '#/components/schemas/WebhookEventBase'
|
|
- type: object
|
|
required:
|
|
- data
|
|
properties:
|
|
type:
|
|
type: string
|
|
const: order.completed
|
|
data:
|
|
type: object
|
|
properties:
|
|
orderId:
|
|
type: string
|
|
format: uuid
|
|
total:
|
|
type: number
|
|
format: double
|
|
currency:
|
|
type: string
|
|
example: USD
|
|
```
|
|
|
|
Document a shared `WebhookEventBase` so all event payloads have a consistent
|
|
envelope with `id`, `type`, and `createdAt`.
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. **Use consistent, plural resource names.** `/users`, `/orders`, `/invoices`
|
|
-- never mix singular and plural within the same API.
|
|
|
|
2. **Make mutating operations idempotent.** Accept an `Idempotency-Key` header
|
|
on POST endpoints so clients can safely retry without creating duplicates.
|
|
|
|
3. **Return rate-limit headers on every response.** Include `X-RateLimit-Limit`,
|
|
`X-RateLimit-Remaining`, and `X-RateLimit-Reset` so clients can self-throttle.
|
|
|
|
4. **Provide `operationId` for every operation.** Code generators use this as
|
|
the method name; without it, generated clients have meaningless names.
|
|
|
|
5. **Include realistic examples in the spec.** Examples power documentation UIs,
|
|
mock servers, and contract tests. Add them at both the schema and operation
|
|
level.
|
|
|
|
6. **Use `additionalProperties: false` on request schemas.** This catches typos
|
|
in client payloads early and prevents silently ignored fields.
|
|
|
|
7. **Document hypermedia links (HATEOAS basics).** Even a minimal `_links`
|
|
object with `self` and `next` URIs helps clients navigate without hardcoding
|
|
paths.
|
|
|
|
8. **Version your spec file alongside code.** Store the OpenAPI document in the
|
|
same repository as the implementation. Run a CI check (e.g., `redocly lint`)
|
|
to validate the spec on every pull request.
|
|
|
|
---
|
|
|
|
## Common Pitfalls
|
|
|
|
1. **Missing error documentation.** Every operation should list its possible
|
|
`4xx` and `5xx` responses. Consumers cannot handle errors they do not know
|
|
about. At minimum document `400`, `401`, `403`, `404`, and `500`.
|
|
|
|
2. **Overusing `200 OK` for everything.** Return `201` for resource creation,
|
|
`204` for deletion, and `202` for asynchronous actions. Correct status codes
|
|
let generic HTTP clients behave properly (e.g., following `Location` headers).
|
|
|
|
3. **Deeply nested resource URLs.** `/users/{uid}/orders/{oid}/items/{iid}/notes`
|
|
is fragile and hard to cache. Flatten to `/order-items/{iid}/notes` once the
|
|
relationship is established.
|
|
|
|
4. **Inconsistent naming conventions.** Mixing `camelCase` and `snake_case`
|
|
within the same API confuses consumers. Pick one JSON field casing and enforce
|
|
it with a linter rule.
|
|
|
|
5. **Ignoring `nullable` vs optional.** In OpenAPI 3.1, `nullable` is gone;
|
|
use `type: ["string", "null"]` instead. A field that is not in `required`
|
|
may be absent, but that is different from being explicitly `null`. Be precise
|
|
about which you intend.
|
|
|
|
6. **No pagination on list endpoints.** Returning unbounded arrays will
|
|
eventually cause timeouts or OOM errors. Every collection endpoint should
|
|
accept `limit` and either `cursor` or `offset` from day one, even if the
|
|
dataset is currently small.
|
|
|
|
---
|
|
|
|
## Related Skills
|
|
|
|
- `patterns/api-client` - Patterns for consuming and generating API clients from specs
|
|
- `patterns/error-handling` - Consistent error response structures and handling
|
|
- `frameworks/fastapi` - FastAPI framework with built-in OpenAPI generation
|