@khanhspring/forge-spec 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install.js +33 -0
- package/forge-brainstorm/SKILL.md +332 -0
- package/forge-close/SKILL.md +129 -0
- package/forge-config/SKILL.md +113 -0
- package/forge-contract/SKILL.md +221 -0
- package/forge-contract/reference/best-practices.md +223 -0
- package/forge-contract/reference/contract-template.yaml +282 -0
- package/forge-init/SKILL.md +205 -0
- package/forge-spec/SKILL.md +279 -0
- package/forge-status/SKILL.md +55 -0
- package/forge-tasks/SKILL.md +79 -0
- package/package.json +22 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "forge-contract"
|
|
3
|
+
description: "Phase 4 of 4. Researches existing contracts for patterns, clarifies ambiguous API design decisions, challenges against REST best practices, then generates a multi-file Specmatic-compatible OpenAPI 3.0 contract on confirmation."
|
|
4
|
+
argument-hint: "Feature slug (e.g. 'user-registration')"
|
|
5
|
+
compatibility: "Requires spec repo with .forge/project.json, features/{slug}/spec.md, and features/{slug}/tasks.md"
|
|
6
|
+
metadata:
|
|
7
|
+
author: "forge-workflow"
|
|
8
|
+
source: "spec-skills/forge-contract/SKILL.md"
|
|
9
|
+
user-invocable: true
|
|
10
|
+
disable-model-invocation: true
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Forge Contract
|
|
14
|
+
|
|
15
|
+
Phase 4 of 4. Understand the API design fully before writing any YAML.
|
|
16
|
+
Research existing contracts, surface ambiguities, challenge against best practices —
|
|
17
|
+
then generate a contract that implementors and consumers can trust.
|
|
18
|
+
|
|
19
|
+
<HARD-GATE>
|
|
20
|
+
Do NOT write any contract files until you have presented a contract outline
|
|
21
|
+
and the user has explicitly confirmed it.
|
|
22
|
+
</HARD-GATE>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Pre-check
|
|
27
|
+
|
|
28
|
+
- Feature slug from $ARGUMENTS, or ask: "Which feature do you want to generate a contract for?"
|
|
29
|
+
- Read `features/{slug}/spec.md` — must exist, otherwise stop.
|
|
30
|
+
- Read `features/{slug}/tasks.md` — must exist, otherwise say "Run `/forge-tasks {slug}` first."
|
|
31
|
+
- Read `.forge/project.json` — get module names, ports, stacks.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Step 1 — Research Context
|
|
36
|
+
|
|
37
|
+
Before asking anything, research silently:
|
|
38
|
+
|
|
39
|
+
1. **Existing contracts for this module** — scan `contracts/{module}/` for existing YAML files.
|
|
40
|
+
Note: field naming convention (camelCase vs snake_case), URL prefix, existing shared schemas,
|
|
41
|
+
existing error format, auth patterns, pagination shape.
|
|
42
|
+
|
|
43
|
+
2. **Shared schemas** — does `contracts/{module}/shared/` exist with `api-error.yaml`,
|
|
44
|
+
`pagination-meta.yaml`, or other reusables? If yes, `$ref` to them — do not redefine.
|
|
45
|
+
|
|
46
|
+
3. **Other module contracts in this feature** — any sibling modules with contracts already written?
|
|
47
|
+
Note shared data models for consistency.
|
|
48
|
+
|
|
49
|
+
4. **Related feature contracts** — earlier features for the same module with schemas to extend
|
|
50
|
+
(e.g. an existing `UserResponse` that this feature adds fields to).
|
|
51
|
+
|
|
52
|
+
5. **Spec API endpoint list** — extract every endpoint from `spec.md` API Endpoints table.
|
|
53
|
+
|
|
54
|
+
6. **Tasks** — extract `[api]` tagged tasks per backend module from `tasks.md`.
|
|
55
|
+
|
|
56
|
+
Report findings:
|
|
57
|
+
|
|
58
|
+
> "Here's what I found:
|
|
59
|
+
> - Existing contracts: {files or 'none yet'}
|
|
60
|
+
> - Shared schemas available: {list or 'none — will create'}
|
|
61
|
+
> - Reusable schemas: {e.g. 'UserResponse in user-auth.yaml' or 'none'}
|
|
62
|
+
> - Endpoints to contract: {list}
|
|
63
|
+
> - Convention: {camelCase/snake_case, /api/v1/ prefix, etc.}
|
|
64
|
+
> - Conflicts: {any mismatch or 'none'}"
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Step 2 — Clarifying Questions (one at a time)
|
|
69
|
+
|
|
70
|
+
Ask ONE question at a time. Only ask about genuine ambiguities not answered by spec or existing contracts.
|
|
71
|
+
|
|
72
|
+
- **HTTP method** — PUT (full replace) or PATCH (partial update)? POST with server ID or PUT with client ID?
|
|
73
|
+
- **Sync vs async** — immediate result (201) or acknowledged and processed later (202)?
|
|
74
|
+
- **Pagination** — which list endpoints need it? page+size, cursor, or offset?
|
|
75
|
+
- **Field types** — IDs: UUID or string? Dates: date-time or date? Enum values?
|
|
76
|
+
- **Required vs optional** — which request fields are mandatory?
|
|
77
|
+
- **Auth scope** — all endpoints require bearer, or are some public?
|
|
78
|
+
- **Error cases** — 403 Forbidden distinct from 401? 409 Conflict needed? 422 for business rules?
|
|
79
|
+
|
|
80
|
+
Use the research flag for anything needing external investigation:
|
|
81
|
+
> "🔍 **Research needed:** {what's unclear}. (a) pause, (b) assume, or (c) flag and move on?"
|
|
82
|
+
|
|
83
|
+
Skip if all decisions are clear.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Step 3 — Challenge Round
|
|
88
|
+
|
|
89
|
+
Flag at least 2 best-practice concerns specific to this feature before drafting:
|
|
90
|
+
|
|
91
|
+
> "Before the outline, a couple of things to flag:
|
|
92
|
+
>
|
|
93
|
+
> **[Concern 1]:** {e.g. 'The spec says GET /users — list endpoints need pagination.
|
|
94
|
+
> Should I add page+size query params and a paginated response wrapper?'}
|
|
95
|
+
>
|
|
96
|
+
> **[Concern 2]:** {e.g. 'The spec says "update user" — is this a full replace (PUT)
|
|
97
|
+
> or partial update (PATCH)? PATCH is safer for partial updates and what the spec implies.'}
|
|
98
|
+
>
|
|
99
|
+
> How should these be handled?"
|
|
100
|
+
|
|
101
|
+
Wait for resolution before the outline.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Step 4 — Contract Outline & Gate
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
109
|
+
Contract Outline: {feature-slug}
|
|
110
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
111
|
+
Module: {module} Port: {port}
|
|
112
|
+
|
|
113
|
+
FILES TO GENERATE
|
|
114
|
+
contracts/{module}/{slug}.yaml ← main contract
|
|
115
|
+
contracts/{module}/schemas/
|
|
116
|
+
create-{resource}-request.yaml
|
|
117
|
+
update-{resource}-request.yaml ← if update endpoint
|
|
118
|
+
{resource}-response.yaml ← for GET single
|
|
119
|
+
{resource}-summary.yaml ← for GET list items
|
|
120
|
+
{resource}-reference.yaml ← for POST 201 response
|
|
121
|
+
enums/{resource}-status.yaml ← if status/type enum
|
|
122
|
+
contracts/{module}/shared/ ← create if not exists
|
|
123
|
+
api-error.yaml
|
|
124
|
+
api-error-detail.yaml
|
|
125
|
+
pagination-meta.yaml ← if list endpoint
|
|
126
|
+
|
|
127
|
+
ENDPOINTS
|
|
128
|
+
POST /api/v1/{resources}
|
|
129
|
+
Auth: bearer
|
|
130
|
+
Request: Create{Resource}Request (no id, no timestamps)
|
|
131
|
+
201: {Resource}Reference (id: uuid only)
|
|
132
|
+
400: ApiError (with details[])
|
|
133
|
+
401: ApiError
|
|
134
|
+
409: ApiError ← if duplicate possible
|
|
135
|
+
|
|
136
|
+
GET /api/v1/{resources}
|
|
137
|
+
Auth: bearer
|
|
138
|
+
Query: page: int, size: int, sort: string, order: asc|desc
|
|
139
|
+
200: { data: {Resource}Summary[], pagination: PaginationMeta }
|
|
140
|
+
401: ApiError
|
|
141
|
+
|
|
142
|
+
GET /api/v1/{resources}/{id}
|
|
143
|
+
Auth: bearer
|
|
144
|
+
200: {Resource}Response (full object)
|
|
145
|
+
401: ApiError
|
|
146
|
+
404: ApiError
|
|
147
|
+
|
|
148
|
+
PATCH /api/v1/{resources}/{id}
|
|
149
|
+
Auth: bearer
|
|
150
|
+
Request: Update{Resource}Request (all fields optional)
|
|
151
|
+
204: (no body)
|
|
152
|
+
400: ApiError (with details[])
|
|
153
|
+
401: ApiError
|
|
154
|
+
403: ApiError ← if ownership check
|
|
155
|
+
404: ApiError
|
|
156
|
+
|
|
157
|
+
DELETE /api/v1/{resources}/{id}
|
|
158
|
+
Auth: bearer
|
|
159
|
+
204: (no body)
|
|
160
|
+
401: ApiError
|
|
161
|
+
403: ApiError
|
|
162
|
+
404: ApiError
|
|
163
|
+
|
|
164
|
+
ASSUMPTIONS
|
|
165
|
+
- {any recorded assumption}
|
|
166
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> "Does this outline look right? Say **yes** to generate, or tell me what to change."
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Step 5 — Generate the contract files
|
|
174
|
+
|
|
175
|
+
Now read both reference files in this skill's folder:
|
|
176
|
+
- `reference/best-practices.md` — schema segregation, HTTP methods, status codes, the
|
|
177
|
+
`ApiError` / `ApiErrorDetail` / `PaginationMeta` shared schemas, enum rules, naming conventions
|
|
178
|
+
- `reference/contract-template.yaml` — the main-contract skeleton (paths + `components/responses`)
|
|
179
|
+
|
|
180
|
+
Then generate every file from the approved outline:
|
|
181
|
+
1. The main contract `contracts/{module}/{slug}.yaml` — use the template skeleton, keeping only
|
|
182
|
+
`info` + `paths` + `$ref`s (no inline schemas).
|
|
183
|
+
2. One file per schema under `schemas/` (and `schemas/enums/` for enums), per the segregation table.
|
|
184
|
+
3. The `shared/` files (`api-error.yaml`, `api-error-detail.yaml`, `pagination-meta.yaml`) —
|
|
185
|
+
only if they don't already exist for this module (reuse if present, per Step 1 research).
|
|
186
|
+
|
|
187
|
+
Apply every rule in `best-practices.md`. Hard requirements:
|
|
188
|
+
- Every endpoint has at least one `examples` block (Specmatic needs them for stubs)
|
|
189
|
+
- `operationId` unique across the file; relative `$ref` paths only
|
|
190
|
+
- Match the field naming convention found in Step 1 (camelCase vs snake_case)
|
|
191
|
+
- Create endpoint → 201 + `Location` header + `{Resource}Reference` (id only); update/delete → 204 no body
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Step 6 — Update spec.md
|
|
196
|
+
|
|
197
|
+
Update the API Endpoints table in `features/{slug}/spec.md`:
|
|
198
|
+
|
|
199
|
+
```markdown
|
|
200
|
+
| POST | /api/v1/{resources} | {module} | [contract](../../contracts/{module}/{slug}.yaml) |
|
|
201
|
+
| GET | /api/v1/{resources} | {module} | [contract](../../contracts/{module}/{slug}.yaml) |
|
|
202
|
+
| GET | /api/v1/{resources}/{id} | {module} | [contract](../../contracts/{module}/{slug}.yaml) |
|
|
203
|
+
| PATCH | /api/v1/{resources}/{id} | {module} | [contract](../../contracts/{module}/{slug}.yaml) |
|
|
204
|
+
| DELETE | /api/v1/{resources}/{id} | {module} | [contract](../../contracts/{module}/{slug}.yaml) |
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Also advance the spec status: change the `**Status:**` line in `features/{slug}/spec.md`
|
|
208
|
+
from `Draft` to `Ready` (planning complete — ready for implementation).
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Step 7 — Completion
|
|
213
|
+
|
|
214
|
+
> "Done. Feature `{slug}` contracts generated:
|
|
215
|
+
>
|
|
216
|
+
> contracts/{module}/
|
|
217
|
+
> {slug}.yaml
|
|
218
|
+
> schemas/ ({n} schema files)
|
|
219
|
+
> shared/ (api-error, pagination-meta)
|
|
220
|
+
>
|
|
221
|
+
> Run `/forge-implement {slug}` in each module repo to start implementation."
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Contract Best Practices
|
|
2
|
+
|
|
3
|
+
Conventions to enforce when generating Specmatic OpenAPI 3.0 contracts.
|
|
4
|
+
Read this together with `contract-template.yaml` (the skeleton).
|
|
5
|
+
|
|
6
|
+
## File structure
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
contracts/
|
|
10
|
+
{module}/
|
|
11
|
+
{feature-slug}.yaml ← main file: info + paths + $refs only
|
|
12
|
+
schemas/
|
|
13
|
+
create-{resource}-request.yaml
|
|
14
|
+
update-{resource}-request.yaml
|
|
15
|
+
{resource}-response.yaml
|
|
16
|
+
{resource}-summary.yaml
|
|
17
|
+
{resource}-reference.yaml
|
|
18
|
+
enums/
|
|
19
|
+
{resource}-status.yaml ← one file per enum
|
|
20
|
+
shared/ ← shared across all features in this module
|
|
21
|
+
api-error.yaml
|
|
22
|
+
api-error-detail.yaml
|
|
23
|
+
pagination-meta.yaml
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The main contract file contains only `info`, `servers`, `security`, `paths`, and
|
|
27
|
+
`components` with `$ref` entries pointing to the schema files.
|
|
28
|
+
Never inline a complex schema in the main file.
|
|
29
|
+
|
|
30
|
+
## Schema segregation — never reuse if even one field differs
|
|
31
|
+
|
|
32
|
+
| Schema | Purpose | Rules |
|
|
33
|
+
|--------|---------|-------|
|
|
34
|
+
| `Create{Resource}Request` | POST body | No `id`, no `createdAt`/`updatedAt`. Only fields the client sets. Required fields required. |
|
|
35
|
+
| `Update{Resource}Request` | PATCH body | All fields optional. No `id`, no timestamps. At least one field must be present (use `minProperties: 1`). |
|
|
36
|
+
| `Replace{Resource}Request` | PUT body | Required fields required. No `id`, no timestamps. |
|
|
37
|
+
| `{Resource}Response` | GET single | Full object. All server-set fields included (`id`, timestamps). |
|
|
38
|
+
| `{Resource}Summary` | GET list item | Lightweight — key fields only (id, name, status, createdAt). No nested objects. |
|
|
39
|
+
| `{Resource}Reference` | POST 201 body | Minimal reference: `id` (uuid) only. Optionally add `href` (the resource URL). |
|
|
40
|
+
|
|
41
|
+
## HTTP method rules
|
|
42
|
+
|
|
43
|
+
| Operation | Method | Path |
|
|
44
|
+
|-----------|--------|------|
|
|
45
|
+
| Create resource | `POST` | `/api/v1/{resources}` |
|
|
46
|
+
| Get single | `GET` | `/api/v1/{resources}/{id}` |
|
|
47
|
+
| Get list | `GET` | `/api/v1/{resources}` |
|
|
48
|
+
| Partial update | `PATCH` | `/api/v1/{resources}/{id}` |
|
|
49
|
+
| Full replace | `PUT` | `/api/v1/{resources}/{id}` |
|
|
50
|
+
| Delete | `DELETE` | `/api/v1/{resources}/{id}` |
|
|
51
|
+
| Custom action | `POST` | `/api/v1/{resources}/{id}/{action}` |
|
|
52
|
+
|
|
53
|
+
- Never use `GET` to mutate state
|
|
54
|
+
- Never put verbs in resource paths (`/deactivateUser` ❌ → `POST /users/{id}/deactivation` ✅)
|
|
55
|
+
- Max 2 nesting levels: `/users/{userId}/addresses/{addressId}` MAX
|
|
56
|
+
- Custom actions as sub-resource nouns: `/activation`, `/suspension`, `/password-reset`
|
|
57
|
+
|
|
58
|
+
## URL path rules
|
|
59
|
+
|
|
60
|
+
- Plural nouns: `/users` not `/user`
|
|
61
|
+
- Kebab-case for multi-word segments: `/user-profiles`, `/order-items`
|
|
62
|
+
- Version prefix: `/api/v1/`
|
|
63
|
+
- No trailing slashes
|
|
64
|
+
- IDs always in path, not query string, when addressing a specific resource
|
|
65
|
+
- UUIDs only — never expose sequential integer IDs in paths
|
|
66
|
+
|
|
67
|
+
## Status code rules
|
|
68
|
+
|
|
69
|
+
| Case | Code |
|
|
70
|
+
|------|------|
|
|
71
|
+
| Successful GET | `200 OK` |
|
|
72
|
+
| Successful POST (resource created) | `201 Created` |
|
|
73
|
+
| Async operation accepted | `202 Accepted` |
|
|
74
|
+
| Successful PATCH / PUT / DELETE (no body) | `204 No Content` |
|
|
75
|
+
| Validation failure (client fixable) | `400 Bad Request` |
|
|
76
|
+
| Not authenticated (missing/invalid token) | `401 Unauthorized` |
|
|
77
|
+
| Authenticated but not authorized | `403 Forbidden` |
|
|
78
|
+
| Resource not found | `404 Not Found` |
|
|
79
|
+
| Duplicate / state conflict | `409 Conflict` |
|
|
80
|
+
| Business rule violation (semantically invalid) | `422 Unprocessable Entity` |
|
|
81
|
+
| Rate limit exceeded | `429 Too Many Requests` |
|
|
82
|
+
| Unexpected server error | `500 Internal Server Error` |
|
|
83
|
+
|
|
84
|
+
## Consistent error object
|
|
85
|
+
|
|
86
|
+
All error responses across all endpoints use `ApiError`.
|
|
87
|
+
Never define a custom error shape per endpoint.
|
|
88
|
+
|
|
89
|
+
**`shared/api-error.yaml`:**
|
|
90
|
+
```yaml
|
|
91
|
+
type: object
|
|
92
|
+
required: [code, message, traceId]
|
|
93
|
+
properties:
|
|
94
|
+
code:
|
|
95
|
+
type: string
|
|
96
|
+
description: "Machine-readable error code (SCREAMING_SNAKE_CASE)"
|
|
97
|
+
example: "VALIDATION_ERROR"
|
|
98
|
+
message:
|
|
99
|
+
type: string
|
|
100
|
+
description: "Human-readable summary"
|
|
101
|
+
example: "Validation failed for 1 field(s)"
|
|
102
|
+
traceId:
|
|
103
|
+
type: string
|
|
104
|
+
format: uuid
|
|
105
|
+
description: "Unique request trace ID for debugging and support"
|
|
106
|
+
example: "123e4567-e89b-12d3-a456-426614174000"
|
|
107
|
+
details:
|
|
108
|
+
type: array
|
|
109
|
+
description: "Field-level errors — present on 400, omitted otherwise"
|
|
110
|
+
items:
|
|
111
|
+
$ref: "./api-error-detail.yaml"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**`shared/api-error-detail.yaml`:**
|
|
115
|
+
```yaml
|
|
116
|
+
type: object
|
|
117
|
+
required: [field, code, message]
|
|
118
|
+
properties:
|
|
119
|
+
field:
|
|
120
|
+
type: string
|
|
121
|
+
description: "JSON path to the invalid field"
|
|
122
|
+
example: "email"
|
|
123
|
+
code:
|
|
124
|
+
type: string
|
|
125
|
+
description: "Field-level error code"
|
|
126
|
+
example: "INVALID_FORMAT"
|
|
127
|
+
message:
|
|
128
|
+
type: string
|
|
129
|
+
description: "What is wrong and how to fix it"
|
|
130
|
+
example: "Must be a valid email address"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Common error codes to use consistently:**
|
|
134
|
+
|
|
135
|
+
| Code | When |
|
|
136
|
+
|------|------|
|
|
137
|
+
| `VALIDATION_ERROR` | 400 — one or more fields invalid (use with `details[]`) |
|
|
138
|
+
| `INVALID_FORMAT` | 400 — field detail: wrong format |
|
|
139
|
+
| `REQUIRED_FIELD` | 400 — field detail: missing required field |
|
|
140
|
+
| `INVALID_VALUE` | 400 — field detail: value not in allowed set |
|
|
141
|
+
| `UNAUTHENTICATED` | 401 |
|
|
142
|
+
| `FORBIDDEN` | 403 |
|
|
143
|
+
| `NOT_FOUND` | 404 |
|
|
144
|
+
| `ALREADY_EXISTS` | 409 — duplicate |
|
|
145
|
+
| `STATE_CONFLICT` | 409 — operation not valid in current state |
|
|
146
|
+
| `BUSINESS_RULE_VIOLATION` | 422 — semantically invalid |
|
|
147
|
+
| `RATE_LIMIT_EXCEEDED` | 429 |
|
|
148
|
+
| `INTERNAL_ERROR` | 500 |
|
|
149
|
+
|
|
150
|
+
## Enum rules
|
|
151
|
+
|
|
152
|
+
- Every enum is a separate file under `schemas/enums/`
|
|
153
|
+
- Values in SCREAMING_SNAKE_CASE: `ACTIVE`, `PENDING_VERIFICATION`, `SOFT_DELETED`
|
|
154
|
+
- Always `$ref` to the enum file — never inline enum values
|
|
155
|
+
- Include a `description` on the enum schema and on each value via `x-enum-descriptions`
|
|
156
|
+
|
|
157
|
+
**Example `schemas/enums/user-status.yaml`:**
|
|
158
|
+
```yaml
|
|
159
|
+
type: string
|
|
160
|
+
description: "Lifecycle status of a user account"
|
|
161
|
+
enum:
|
|
162
|
+
- ACTIVE
|
|
163
|
+
- PENDING_VERIFICATION
|
|
164
|
+
- SUSPENDED
|
|
165
|
+
- DELETED
|
|
166
|
+
x-enum-descriptions:
|
|
167
|
+
ACTIVE: "Account is active and can log in"
|
|
168
|
+
PENDING_VERIFICATION: "Email not yet verified"
|
|
169
|
+
SUSPENDED: "Account suspended by admin"
|
|
170
|
+
DELETED: "Soft-deleted, not visible to users"
|
|
171
|
+
example: "ACTIVE"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Naming conventions
|
|
175
|
+
|
|
176
|
+
| Item | Convention | Example |
|
|
177
|
+
|------|-----------|---------|
|
|
178
|
+
| Schema names | PascalCase | `CreateUserRequest`, `UserResponse` |
|
|
179
|
+
| Properties | camelCase | `firstName`, `createdAt`, `isActive` |
|
|
180
|
+
| Enum values | SCREAMING_SNAKE_CASE | `PENDING_VERIFICATION` |
|
|
181
|
+
| Operation IDs | camelCase verb+noun | `createUser`, `listUsers`, `getUserById` |
|
|
182
|
+
| File names | kebab-case | `create-user-request.yaml` |
|
|
183
|
+
| Error codes | SCREAMING_SNAKE_CASE | `VALIDATION_ERROR` |
|
|
184
|
+
|
|
185
|
+
## Additional rules
|
|
186
|
+
|
|
187
|
+
- Never return `null` fields — omit absent optional fields from responses
|
|
188
|
+
- IDs are always `type: string, format: uuid` — never integer
|
|
189
|
+
- Dates always `type: string, format: date-time` (ISO 8601 with timezone)
|
|
190
|
+
- List endpoints always support `page` (int, default 1), `size` (int, default 20, max 100)
|
|
191
|
+
- List responses always include `PaginationMeta`: `page`, `size`, `total`, `totalPages`
|
|
192
|
+
- POST 201 response includes `Location` header pointing to the created resource URL
|
|
193
|
+
|
|
194
|
+
**`shared/pagination-meta.yaml`:**
|
|
195
|
+
```yaml
|
|
196
|
+
type: object
|
|
197
|
+
required: [page, size, total, totalPages]
|
|
198
|
+
properties:
|
|
199
|
+
page:
|
|
200
|
+
type: integer
|
|
201
|
+
description: "Current page (1-based)"
|
|
202
|
+
example: 1
|
|
203
|
+
size:
|
|
204
|
+
type: integer
|
|
205
|
+
description: "Items per page"
|
|
206
|
+
example: 20
|
|
207
|
+
total:
|
|
208
|
+
type: integer
|
|
209
|
+
description: "Total number of items"
|
|
210
|
+
example: 142
|
|
211
|
+
totalPages:
|
|
212
|
+
type: integer
|
|
213
|
+
description: "Total number of pages"
|
|
214
|
+
example: 8
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Specmatic rules
|
|
218
|
+
|
|
219
|
+
- Every endpoint MUST have at least one `examples` block — Specmatic uses these for stubs
|
|
220
|
+
- `operationId` must be unique across the entire file
|
|
221
|
+
- Include `format` for every applicable property: `uuid`, `date`, `date-time`, `email`, `uri`
|
|
222
|
+
- One main contract file per module per feature — never one file per endpoint
|
|
223
|
+
- All `$ref` paths must be relative (e.g. `./schemas/user-response.yaml`, `../shared/api-error.yaml`)
|