- name
- openapi-schema-best-practices
- description
- Create, audit, and maintain OpenAPI schemas in meshery/schemas following established conventions. Use this skill whenever working with OpenAPI YAML files, adding new API constructs, reviewing schema consistency, checking naming conventions, validating $ref patterns, or understanding how schemas flow through the Go and TypeScript code generators. Also trigger when the user mentions schema audits, API consistency checks, schema linting, construct definitions, or asks about the build pipeline (bundle-openapi, generate-golang, generate-typescript). If the user is creating a schema from a Golang model, prefer the create-openapi-schemas-from-golang-models skill instead.
# OpenAPI Schema Best Practices
> Canonical naming contract - see `docs/identifier-naming-contributor-guide.md` in `meshery/schemas` (<https://github.com/meshery/schemas/blob/master/docs/identifier-naming-contributor-guide.md>) for the full directory (26-row naming table with before/after and do/don't examples). The inline rules below remain the skill's authority for its workflow scope; the guide is the reader-friendly cross-repo reference.
You are an expert in Meshery's Schema-Driven Development (SDD) system. Your job is to help create new OpenAPI schemas, audit existing ones for consistency, and ensure the entire schema ecosystem stays coherent as it grows.
Before doing any schema work, read `.claude/agents/code-contributor.md` and `AGENTS.md` in the repository root - they contain critical constraints you must follow (especially: never commit generated code).
## How this repository works
Meshery defines its data model as OpenAPI 3.0 YAML schemas under `schemas/constructs/`. A build pipeline then generates Go structs and TypeScript types from those schemas automatically. The schemas are the single source of truth - never hand-edit generated files.
### Build pipeline overview
```
schemas/constructs/**/*.{yaml,yml} (you write these)
│
▼
bundle-openapi.js (dereferences + merges in-process)
│
▼
_openapi_build/**/*.json (intermediate bundled JSON)
│
├──▶ generate-golang.js (reads source api.yml packages + reachable refs)
│ └──▶ models/**/*.go (oapi-codegen)
├──▶ generate-typescript.js → typescript/generated/ (openapi-typescript)
└──▶ generate-rtk.js → typescript/rtk/ (RTK Query hooks)
```
Note: `generate-golang.js` reads source `api.yml` files directly (not the bundled JSON), but the standard build (`make build`) still runs `bundle-openapi` first because Go generation depends on it in both the Makefile and `build/index.js`.
Understanding this pipeline matters because schema design decisions directly affect the quality of generated code. A poorly structured schema produces awkward Go structs and confusing TypeScript types.
### How code generators consume schemas
**Go generator** (`build/generate-golang.js`):
- Reads source `schemas/constructs/**/api.yml` packages directly
- Stages temporary package-local specs and rewrites reachable cross-package refs
- Generates Go structs with JSON + YAML struct tags
- Collects `x-oapi-codegen-extra-tags`, `x-go-name`, and `x-go-type` metadata across direct `$ref` and `allOf` composition
- Builds import mappings from external `$ref` targets so cross-package types resolve correctly
- Rewrites external import aliases using explicit `x-go-type-import.name` values when provided
- Derives repetitive Go helper methods from generated package/type structure instead of relying on a hand-maintained package manifest
- Uses `oapi-codegen` v2.x (pinned via `tool` directive in `go.mod`) under the hood
**TypeScript generator** (`build/generate-typescript.js`):
- Reads the same bundled JSON
- Produces TypeScript type definitions via `openapi-typescript`
- Also exports JSON schema as TypeScript const objects (`*Schema.ts`)
**RTK Query generator** (`build/generate-rtk.js`):
- Reads filtered merged specs (`cloud_openapi.yml`, `meshery_openapi.yml`)
- Generates RTK Query hooks from the `paths` defined in `api.yml` files
- Tags endpoints using `x-internal` to split between cloud and meshery APIs
### Package discovery
The build system discovers packages dynamically by walking `schemas/constructs/<version>/` and finding directories that contain an `api.yml` file. This means:
- Every new construct needs an `api.yml` to be picked up by the build
- The directory name becomes the package name (with some overrides, e.g. `design` → `pattern`)
- Some packages are excluded from merging: `v1alpha1/core` and `v1alpha1/capability`
## Directory structure for a construct
```
schemas/constructs/<version>/<construct>/
├── api.yml # Index file (required): aggregates subschemas + defines API paths
├── <construct>.yaml # Subschema: main data definitions
├── <construct>_core.yml # Subschema: core definitions (optional)
└── templates/
├── <construct>_template.json # Default-value template
├── <construct>_template.yaml # YAML variant (optional)
└── <construct>_minimal_template.json # Minimal variant (optional)
```
The `api.yml` is the entry point. It references subschemas via `$ref` and defines REST endpoints under `paths:`. Code generators only read `api.yml` - subschemas are pulled in through references.
## Naming conventions
These conventions apply to all new additions (properties, paths, operationIds, etc.) for consistency across APIs. Some legacy and DB-mirrored fields are explicit exceptions, as noted below:
| Element | Convention | Examples |
|---------|-----------|----------|
| Non-DB-mirrored schema property names | camelCase | `schemaVersion`, `displayName`, `componentsCount` |
| Identifier fields | camelCase + "Id" suffix | `modelId`, `registrantId`, `categoryId` |
| New enum values | lowercase | `enabled`, `ignored`, `duplicate` |
| Schema component names | PascalCase | `ModelDefinition`, `ComponentDefinition` |
| File/folder names | lowercase, underscores OK | `model.yaml`, `model_core.yml`, `api.yml` |
| API paths | `/api` prefix, kebab-case, plural nouns | `/api/workspaces`, `/api/environments` |
| Path parameters | camelCase | `{subscriptionId}`, `{connectionId}` |
| operationId | lower camelCase verbNoun | `getAllRoles`, `listUsers` |
| Version strings | k8s-style | `v1alpha1`, `v1beta1` |
| schemaVersion | group/version | `models.meshery.io/v1beta1` |
Path parameters must use camelCase with the `Id` suffix - never SCREAMING\_CASE or snake\_case:
| Correct | Wrong |
|---|---|
| `{orgId}` | `{orgID}`, `{org_id}` |
| `{workspaceId}` | `{workspaceID}` |
| `{connectionId}` | `{connectionID}`, `{connection_id}` |
## Casing rules - single authoritative reference
Every element has exactly one correct casing. Use this table for all decisions:
| Element | Casing | Example | Counter-example |
|---|---|---|---|
| Schema property names (non-DB) | camelCase | `schemaVersion`, `displayName` | ~~`schema_version`~~, ~~`SchemaVersion`~~ |
| ID-suffix properties | camelCase + `Id` | `modelId`, `registrantId` | ~~`modelID`~~, ~~`model_id`~~ |
| **DB-backed / DB-mirrored fields** | **exact snake\_case db column name** | `created_at`, `updated_at`, `user_id`, `first_name`, `plan_id` | ~~`createdAt`~~, ~~`firstName`~~, ~~`planId`~~ |
| New enum values | lowercase | `enabled`, `ignored` | ~~`Enabled`~~, ~~`ENABLED`~~ |
| `components/schemas` names | PascalCase | `ModelDefinition`, `KeychainPayload` | ~~`modelDefinition`~~ |
| File and folder names | lowercase | `api.yml`, `keychain.yaml` | ~~`Keychain.yaml`~~ |
| Path segments | kebab-case plural nouns | `/api/role-holders` | ~~`/api/roleHolders`~~ |
| Path parameters | camelCase + `Id` suffix | `{orgId}`, `{workspaceId}` | ~~`{orgID}`~~, ~~`{org_id}`~~ |
| `operationId` | lower camelCase verbNoun | `getAllRoles`, `createWorkspace` | ~~`GetAllRoles`~~, ~~`get_all_roles`~~ |
| Go type names | PascalCase (generated) | `Connection`, `KeychainPayload` | - |
| TypeScript type names | PascalCase (generated) | `Connection`, `KeychainPayload` | - |
**The database naming is the compatibility boundary.** If a property has `x-oapi-codegen-extra-tags.db` and that `db` value is snake_case, then the schema property name and JSON tag must use that exact snake_case name. Do not camelize DB-backed fields in-place within an existing API version.
**Partial casing migrations are forbidden.** Do not rename selected fields within the same resource from snake_case to camelCase while leaving other published fields unchanged. If the wire format must change, introduce a new API version and migrate the resource consistently there.
**Existing enum wire values are compatibility-sensitive.** Use lowercase for newly introduced enum literals, but do not recase published enum values in-place within the same API version. The validator exempts legacy enum values that already exist on the baseline branch.
**Pagination envelopes are fixed API contract fields** - use `page`, `page_size`, and `total_count`, not `pageSize` or `totalCount`.
**Exceptions for DB-mirrored/system fields**
Some fields intentionally remain `snake_case` to mirror existing database columns and historical schemas. Common examples are `created_at`, `updated_at`, and `user_id`. Do **not** rename these to camelCase in existing schemas. These DB-mirrored/system fields are the only allowed `snake_case` properties; all other (non-DB-mirrored) property names MUST follow the camelCase rules above.
## Common schema references
The `v1alpha1/core/api.yml` file defines reusable building blocks. Always reference these instead of redefining them:
```yaml
# Timestamps - use the core refs, not inline definitions
created_at:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/created_at
x-order: 14
updated_at:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/updated_at
x-order: 15
# UUIDs
id:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/uuid
# Version strings
version:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/versionString
# Semver
semver:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/semverString
```
When using a `$ref` to a core schema that already defines `x-oapi-codegen-extra-tags`, do NOT add redundant tags - they're already in the core definition.
For schemas in `v1alpha3`, the relative path is shorter: `../v1alpha1/core/api.yml#/...`
## Schema design patterns
## HTTP API Design Principles
These rules govern how endpoints are structured. Violations are caught by `make validate-schemas`.
### HTTP method semantics
| Use case | Method | Example |
|---|---|---|
| Create a resource | `POST` | `POST /api/workspaces` → **201** |
| Upsert (create or update) | `POST` | `POST /api/keys` → 200 |
| Update an existing resource | `PUT` or `PATCH` | `PUT /api/workspaces/{workspaceId}` → 200 |
| Non-CRUD action | `POST` to a sub-resource | `POST /api/invitations/{invitationId}/accept` → 200 |
| Bulk delete | `POST` to a `/delete` sub-resource | `POST /api/designs/delete` → 200 |
| Single delete | `DELETE` | `DELETE /api/keys/{keyId}` → 204 |
**Critical: Never use `DELETE` with a request body.** REST semantics don't define request bodies for `DELETE`; HTTP clients and proxies may strip them silently. Bulk deletes must use `POST /api/{resources}/delete`.
```yaml
# WRONG - DELETE with a body; clients/proxies may silently strip it
delete:
operationId: deletePatterns
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatternIds'
# CORRECT - POST sub-resource for bulk delete
post:
operationId: deletePatterns
summary: Bulk delete patterns by ID
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatternIds'
responses:
"200":
description: Patterns deleted
```
### HTTP response codes
| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | Request succeeded; body contains result (queries, upserts, actions) |
| 201 | Created | A new resource was created; body contains the new resource |
| 202 | Accepted | Request accepted; operation completes asynchronously |
| 204 | No Content | Request succeeded; no response body (e.g., single-resource `DELETE`) |
Use 201 (not 200) when a `POST` endpoint exclusively creates a new resource.
Response descriptions and response message text must not include the word `successfully`. Use neutral wording such as `Connection deleted`, `Webhook processed`, or `Plans response`.
### Resource grouping
Endpoints are grouped into logical categories:
| Category prefix | Domain |
|---|---|
| `/api/identity/` | Users, orgs, roles, teams, invitations |
| `/api/integrations/` | Connections, environments, credentials |
| `/api/content/` | Designs, views, components, models |
| `/api/entitlement/` | Plans, subscriptions, features |
| `/api/auth/` | Tokens, keychains, keys |
New endpoints must be placed in the appropriate category. Path segments are kebab-case plural nouns.
---
### The Dual-Schema Pattern (REQUIRED for all entity schemas)
Every persisted entity MUST follow this pattern. Violating it causes generated Go structs and API clients in downstream repos (`meshery/meshery`, `layer5io/meshery-cloud`) to require clients to supply server-generated fields.
#### Rule 1 - `<construct>.yaml` is a response schema only
The YAML file is the **full server-side object** as returned in API responses. It is never a request body.
Requirements:
- `additionalProperties: false` at the top level
- All server-generated fields (`id`, `created_at`, `updated_at`, `deleted_at`) in `properties`
- Server-generated fields that are always present in responses listed in `required`
```yaml
# CORRECT: keychain.yaml
type: object
additionalProperties: false
required:
- id
- name
- owner
- created_at
- updated_at
properties:
id:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/uuid
name:
type: string
owner:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/uuid
created_at:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/created_at
updated_at:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/updated_at
deleted_at:
$ref: ../../v1alpha1/core/api.yml#/components/schemas/nullTime
```
#### Rule 2 - Define a `{Construct}Payload` in `api.yml` for every writable entity
Ver en GitHub