Golang E-Commerce OpenAPI Contract: API Documentation from an SDD Spec
Build a complete OpenAPI contract for Santekno Shop from an SDD spec. Swagger/OpenAPI 3.0, code generation, mock server, and contract testing for a Go e-commerce app.
OpenAPI Contracts for Every Santekno Shop Service
Building an OpenAPI contract for a Golang e-commerce app means defining the API before the code is written. The OpenAPI spec is the official contract between backend and frontend — the frontend can start developing while we implement the backend. In this article, we convert the spec from the first article into a complete OpenAPI 3.0, then use it for code generation, a mock server, and contract testing.
Contract-first is not just documentation. It becomes the single source of truth that binds the backend, the frontend, and CI to one shared API shape — so integration stops being an endless source of bugs.
03.01 Why an OpenAPI Contract Matters
Before getting technical, it is important to understand the problem the contract solves. The comparison below shows the chaos without a contract versus the order with OpenAPI.
1WITHOUT AN API CONTRACT:
2 Backend developer: "I return snake_case"
3 Frontend developer: "Wait, I expected camelCase"
4 → Wasted integration time
5
6 Backend: "The error message is 'order.not_found'"
7 Frontend: "I coded for 'ORDER_NOT_FOUND'"
8 → Bug in production
9
10WITH AN OpenAPI CONTRACT:
11 One source of truth for the API shape
12 Frontend can generate TypeScript types from the spec
13 Backend can validate responses against the spec
14 Mock server from the spec → frontend develops in parallel
15 Contract testing → reliable integrations
16
17Specific benefits for Santekno Shop:
18 → SDD spec → OpenAPI spec → Go server code
19 → CI validates the response format against OpenAPI
20 → The frontend team does not need to wait for the backend to finishThe most important point: the contract moves mismatch discovery (snake_case vs camelCase, different error codes) from the integration phase to the design phase — long before code meets in production.
03.02 Tooling Setup
To work with OpenAPI, we need a few tools. The commands below install the code generator, validator, mock server, and TypeScript generator all at once.
1# Tools used:
2
3# 1. oapi-codegen: generate Go server code from OpenAPI
4go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
5
6# 2. swagger-cli: validate the OpenAPI spec
7npm install -g @apidevtools/swagger-cli
8
9# 3. prism: mock server from OpenAPI
10npm install -g @stoplight/prism-cli
11
12# 4. openapi-typescript: generate TypeScript types (for the frontend)
13npm install -g openapi-typescript
14
15# Verify:
16oapi-codegen --version
17swagger-cli --version
18prism --versionThese four tools close the entire contract-first cycle: writing, validating, generating code, and providing a mock. Running the verification block ensures everything is ready before writing the spec.
03.03 OpenAPI 3.0 Spec: Auth Endpoints
We start with the authentication endpoints. The YAML spec below defines the security scheme, the common schemas, and the register/login endpoints complete with example responses.
1# api/openapi/santekno-shop.yaml
2openapi: '3.0.0'
3info:
4 title: Santekno Shop API
5 version: '1.0.0'
6 description: |
7 B2C E-Commerce Platform Indonesia.
8 All monetary values are in IDR cents (1 IDR = 100 cents).
9 All IDs are UUIDs.
10
11servers:
12 - url: http://localhost:8080/api/v1
13 description: Development
14 - url: https://staging.santekno.com/api/v1
15 description: Staging
16
17components:
18 securitySchemes:
19 BearerAuth:
20 type: http
21 scheme: bearer
22 bearerFormat: JWT
23
24 schemas:
25 ErrorResponse:
26 type: object
27 required: [error]
28 properties:
29 error:
30 type: string
31 example: "ORDER_NOT_FOUND"
32 description: Machine-readable error code
33 detail:
34 type: string
35 example: "Order with ID xxx was not found"
36 description: Human-readable description
37
38 PaginationMeta:
39 type: object
40 properties:
41 page: { type: integer, example: 1 }
42 limit: { type: integer, example: 10 }
43 total: { type: integer, example: 100 }
44 total_pages: { type: integer, example: 10 }
45
46 UUID:
47 type: string
48 format: uuid
49 example: "550e8400-e29b-41d4-a716-446655440000"
50
51 AmountIDR:
52 type: integer
53 format: int64
54 description: "Amount in IDR cents. 1 IDR = 100 cents. Rp 50.000 = 5000000"
55 example: 5000000
56
57 UserResponse:
58 type: object
59 properties:
60 id: { $ref: '#/components/schemas/UUID' }
61 email: { type: string, format: email }
62 name: { type: string }
63 created_at: { type: string, format: date-time }
64
65 RegisterRequest:
66 type: object
67 required: [email, password, name]
68 properties:
69 email:
70 type: string
71 format: email
72 example: "user@example.com"
73 password:
74 type: string
75 minLength: 8
76 example: "Secret123"
77 description: "Minimum 8 characters, must contain at least 1 digit"
78 name:
79 type: string
80 minLength: 2
81 maxLength: 100
82 example: "John Doe"
83
84 LoginRequest:
85 type: object
86 required: [email, password]
87 properties:
88 email: { type: string, format: email }
89 password: { type: string }
90
91 TokenResponse:
92 type: object
93 properties:
94 access_token: { type: string }
95 refresh_token: { type: string }
96 expires_in: { type: integer, example: 3600 }
97 token_type: { type: string, example: "Bearer" }
98
99paths:
100 /auth/register:
101 post:
102 tags: [Auth]
103 summary: Register new user
104 operationId: registerUser
105 requestBody:
106 required: true
107 content:
108 application/json:
109 schema: { $ref: '#/components/schemas/RegisterRequest' }
110 responses:
111 '201':
112 description: User registered successfully
113 content:
114 application/json:
115 schema: { $ref: '#/components/schemas/UserResponse' }
116 '400':
117 description: Validation error
118 content:
119 application/json:
120 schema: { $ref: '#/components/schemas/ErrorResponse' }
121 '409':
122 description: Email already registered
123 content:
124 application/json:
125 schema: { $ref: '#/components/schemas/ErrorResponse' }
126 example: { error: "EMAIL_ALREADY_REGISTERED" }
127
128 /auth/login:
129 post:
130 tags: [Auth]
131 summary: Login and get tokens
132 operationId: loginUser
133 requestBody:
134 required: true
135 content:
136 application/json:
137 schema: { $ref: '#/components/schemas/LoginRequest' }
138 responses:
139 '200':
140 description: Login successful
141 content:
142 application/json:
143 schema: { $ref: '#/components/schemas/TokenResponse' }
144 '401':
145 description: Invalid credentials
146 content:
147 application/json:
148 schema: { $ref: '#/components/schemas/ErrorResponse' }
149 example: { error: "INVALID_CREDENTIALS" }
150 '429':
151 description: Too many attempts
152 content:
153 application/json:
154 schema: { $ref: '#/components/schemas/ErrorResponse' }
155 example: { error: "TOO_MANY_ATTEMPTS", detail: "retry_after: 900" }Every status code in this spec exactly mirrors an AC from the SDD spec: 409 for a registered email, 429 for the rate limit. The reusable AmountIDR and ErrorResponse schemas ensure consistency across every subsequent endpoint.
03.04 OpenAPI Spec: Order Endpoints
The order endpoints are more complex because they involve an array of items and various business errors. The YAML spec below defines the order schema along with the place, list, and cancel endpoints.
1# (continuation of api/openapi/santekno-shop.yaml)
2
3components:
4 schemas:
5 OrderItem:
6 type: object
7 properties:
8 product_id: { $ref: '#/components/schemas/UUID' }
9 product_name: { type: string }
10 unit_price_idr: { $ref: '#/components/schemas/AmountIDR' }
11 quantity: { type: integer, minimum: 1 }
12 subtotal_idr: { $ref: '#/components/schemas/AmountIDR' }
13
14 OrderStatus:
15 type: string
16 enum: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
17
18 OrderResponse:
19 type: object
20 properties:
21 id: { $ref: '#/components/schemas/UUID' }
22 status: { $ref: '#/components/schemas/OrderStatus' }
23 items:
24 type: array
25 items: { $ref: '#/components/schemas/OrderItem' }
26 total_amount_idr: { $ref: '#/components/schemas/AmountIDR' }
27 payment_method: { type: string, enum: [BANK_TRANSFER, QRIS, CREDIT_CARD] }
28 payment_url: { type: string, format: uri, nullable: true }
29 created_at: { type: string, format: date-time }
30 updated_at: { type: string, format: date-time }
31
32 PlaceOrderRequest:
33 type: object
34 required: [items, shipping_address_id, payment_method]
35 properties:
36 items:
37 type: array
38 minItems: 1
39 maxItems: 10
40 items:
41 type: object
42 required: [product_id, quantity]
43 properties:
44 product_id: { $ref: '#/components/schemas/UUID' }
45 quantity:
46 type: integer
47 minimum: 1
48 maximum: 100
49 shipping_address_id: { $ref: '#/components/schemas/UUID' }
50 payment_method:
51 type: string
52 enum: [BANK_TRANSFER, QRIS, CREDIT_CARD]
53
54 OrderListResponse:
55 type: object
56 properties:
57 data:
58 type: array
59 items: { $ref: '#/components/schemas/OrderResponse' }
60 pagination: { $ref: '#/components/schemas/PaginationMeta' }
61
62paths:
63 /orders:
64 get:
65 tags: [Orders]
66 summary: Get user's orders
67 security: [{ BearerAuth: [] }]
68 operationId: getOrders
69 parameters:
70 - name: page
71 in: query
72 schema: { type: integer, default: 1, minimum: 1 }
73 - name: limit
74 in: query
75 schema: { type: integer, default: 10, minimum: 1, maximum: 100 }
76 - name: status
77 in: query
78 schema: { $ref: '#/components/schemas/OrderStatus' }
79 responses:
80 '200':
81 description: List of orders
82 content:
83 application/json:
84 schema: { $ref: '#/components/schemas/OrderListResponse' }
85 '401':
86 description: Unauthorized
87 content:
88 application/json:
89 schema: { $ref: '#/components/schemas/ErrorResponse' }
90 example: { error: "UNAUTHORIZED" }
91
92 post:
93 tags: [Orders]
94 summary: Place a new order
95 security: [{ BearerAuth: [] }]
96 operationId: placeOrder
97 requestBody:
98 required: true
99 content:
100 application/json:
101 schema: { $ref: '#/components/schemas/PlaceOrderRequest' }
102 responses:
103 '201':
104 description: Order placed successfully
105 content:
106 application/json:
107 schema: { $ref: '#/components/schemas/OrderResponse' }
108 '400':
109 description: Invalid request
110 content:
111 application/json:
112 schema: { $ref: '#/components/schemas/ErrorResponse' }
113 example: { error: "EMPTY_ORDER" }
114 '404':
115 description: Product not found
116 content:
117 application/json:
118 schema: { $ref: '#/components/schemas/ErrorResponse' }
119 example: { error: "PRODUCT_NOT_FOUND", detail: "product_id: xxx" }
120 '422':
121 description: Insufficient stock
122 content:
123 application/json:
124 schema: { $ref: '#/components/schemas/ErrorResponse' }
125 example: { error: "INSUFFICIENT_STOCK", detail: "product_id: xxx" }
126
127 /orders/{orderId}:
128 delete:
129 tags: [Orders]
130 summary: Cancel an order
131 security: [{ BearerAuth: [] }]
132 operationId: cancelOrder
133 parameters:
134 - name: orderId
135 in: path
136 required: true
137 schema: { $ref: '#/components/schemas/UUID' }
138 responses:
139 '204':
140 description: Order cancelled successfully
141 '404':
142 description: Order not found
143 content:
144 application/json:
145 schema: { $ref: '#/components/schemas/ErrorResponse' }
146 example: { error: "ORDER_NOT_FOUND" }
147 '409':
148 description: Order cannot be cancelled
149 content:
150 application/json:
151 schema: { $ref: '#/components/schemas/ErrorResponse' }
152 examples:
153 not_cancellable:
154 value: { error: "ORDER_NOT_CANCELLABLE" }
155 window_expired:
156 value: { error: "CANCEL_WINDOW_EXPIRED" }The minItems: 1 and maxItems: 10 constraints on items, along with maximum: 100 on quantity, enforce the business rules from the spec directly at the contract level. This validation automatically applies to both the mock server and the generated code.
03.05 OpenAPI Spec: Product Endpoints
The product endpoints complete the catalog with filter and search. The YAML spec below defines the product schema as well as the list and detail endpoints.
1components:
2 schemas:
3 ProductResponse:
4 type: object
5 properties:
6 id: { $ref: '#/components/schemas/UUID' }
7 name: { type: string }
8 description: { type: string, nullable: true }
9 price_idr: { $ref: '#/components/schemas/AmountIDR' }
10 stock: { type: integer, minimum: 0 }
11 category:
12 type: object
13 properties:
14 id: { $ref: '#/components/schemas/UUID' }
15 name: { type: string }
16 images:
17 type: array
18 items: { type: string, format: uri }
19 maxItems: 10
20 is_active: { type: boolean }
21 created_at: { type: string, format: date-time }
22
23 ProductListResponse:
24 type: object
25 properties:
26 data:
27 type: array
28 items: { $ref: '#/components/schemas/ProductResponse' }
29 pagination: { $ref: '#/components/schemas/PaginationMeta' }
30
31paths:
32 /products:
33 get:
34 tags: [Products]
35 summary: List products with filters
36 operationId: listProducts
37 parameters:
38 - name: q
39 in: query
40 description: Search query (full-text search)
41 schema: { type: string }
42 - name: category
43 in: query
44 schema: { $ref: '#/components/schemas/UUID' }
45 - name: min_price
46 in: query
47 description: Minimum price in IDR cents
48 schema: { type: integer, minimum: 0 }
49 - name: max_price
50 in: query
51 description: Maximum price in IDR cents
52 schema: { type: integer, minimum: 0 }
53 - name: page
54 in: query
55 schema: { type: integer, default: 1 }
56 - name: limit
57 in: query
58 schema: { type: integer, default: 10, maximum: 50 }
59 responses:
60 '200':
61 description: List of products
62 content:
63 application/json:
64 schema: { $ref: '#/components/schemas/ProductListResponse' }
65
66 /products/{productId}:
67 get:
68 tags: [Products]
69 summary: Get product detail
70 operationId: getProduct
71 parameters:
72 - name: productId
73 in: path
74 required: true
75 schema: { $ref: '#/components/schemas/UUID' }
76 responses:
77 '200':
78 description: Product detail
79 content:
80 application/json:
81 schema: { $ref: '#/components/schemas/ProductResponse' }
82 '404':
83 description: Product not found
84 content:
85 application/json:
86 schema: { $ref: '#/components/schemas/ErrorResponse' }
87 example: { error: "NOT_FOUND" }The product endpoints deliberately do not use security because the catalog can be read publicly — consistent with the auth rules in the spec. Reusing AmountIDR and PaginationMeta here shows the value of centralized component schemas.
03.06 Validating the OpenAPI Spec
An invalid spec will mislead every downstream tool. The commands below validate the spec and install it as a pre-commit hook so that drift is caught as early as possible.
1# Validate the spec before commit
2swagger-cli validate api/openapi/santekno-shop.yaml
3
4# Expected output: api/openapi/santekno-shop.yaml is valid
5# If there's an error: fix it before commit
6
7# Add to pre-commit:
8# .pre-commit-config.yaml
9repos:
10 - repo: local
11 hooks:
12 - id: validate-openapi
13 name: Validate OpenAPI Spec
14 language: system
15 entry: swagger-cli validate api/openapi/santekno-shop.yaml
16 files: 'api/openapi/.*\.yaml'Installing validation as a pre-commit hook means a broken spec will never make it into the repository. This is the earliest gate in the chain of drift prevention.
03.07 Code Generation from OpenAPI
The true power of OpenAPI appears when the spec produces Go code automatically. The command and config below run oapi-codegen to produce types and a server interface.
1# oapi-codegen config
2# api/openapi/gen-config.yaml
3
4package: api
5generate:
6 - types # Generate Go types from schemas
7 - server # Generate Echo server interface
8 - client # Generate HTTP client (for testing)
9 - spec # Embed spec in binary
10
11output: internal/api/generated.goWith the OpenAPI contract ready, the step below generates the Go server code directly from that specification.
1# Generate code:
2oapi-codegen \
3 --config api/openapi/gen-config.yaml \
4 api/openapi/santekno-shop.yaml
5
6# Output: internal/api/generated.go
7# Contains:
8# - RegisterRequest, LoginRequest, TokenResponse structs
9# - OrderResponse, PlaceOrderRequest structs
10# - ProductResponse, ProductListResponse structs
11# - ServerInterface (implemented by the handler)The config tells the generator to produce four artifacts at once: types, server interface, client, and embedded spec. The generation result is Go code like the following.
1// internal/api/generated.go (sample generated code)
2// DO NOT edit manually — regenerated from the spec
3
4// RegisterRequest defines model for RegisterRequest.
5type RegisterRequest struct {
6 Email string `json:"email"`
7 Name string `json:"name"`
8 Password string `json:"password"`
9}
10
11// OrderResponse defines model for OrderResponse.
12type OrderResponse struct {
13 CreatedAt time.Time `json:"created_at"`
14 Id uuid.UUID `json:"id"`
15 Items []OrderItem `json:"items"`
16 PaymentMethod string `json:"payment_method"`
17 PaymentUrl *string `json:"payment_url"`
18 Status OrderStatus `json:"status"`
19 TotalAmountIdr int64 `json:"total_amount_idr"`
20 UpdatedAt time.Time `json:"updated_at"`
21}Note the TotalAmountIdr int64 — the int64 cents convention from the spec automatically appears in the Go code. Because ServerInterface is also generated, a handler that does not match the contract will fail to compile: contract enforcement at the compiler level.
03.08 Mock Server with Prism
The frontend does not need to wait for the backend to finish. The commands below start a mock server from the spec, complete with how to trigger both success and error scenarios.
1# Start a mock server from the OpenAPI spec
2prism mock api/openapi/santekno-shop.yaml --port 4010
3
4# The mock server is now available at localhost:4010
5# The frontend can develop while the backend is being implemented
6
7# Test the mock server:
8curl -X POST http://localhost:4010/api/v1/auth/register \
9 -H "Content-Type: application/json" \
10 -d '{"email": "test@example.com", "password": "Secret123", "name": "Test User"}'
11
12# Response: 201 Created with generated fake data
13# {
14# "id": "550e8400-e29b-41d4-a716-446655440000",
15# "email": "test@example.com",
16# "name": "Test User",
17# "created_at": "2026-07-20T10:00:00Z"
18# }
19
20# Test an error scenario with the prefer header:
21curl -X POST http://localhost:4010/api/v1/auth/register \
22 -H "Content-Type: application/json" \
23 -H "Prefer: code=409" \
24 -d '{"email": "existing@example.com"}'
25# Response: 409 ConflictWith the Prefer: code=409 header, the frontend can even test error paths without any backend at all. The mock server lets both teams work fully in parallel — the biggest time saver of contract-first.
03.09 Contract Testing
After the backend is implemented, we need to ensure its responses truly match the contract. The Go code below loads the OpenAPI spec and verifies the register endpoint response against it.
1// internal/api/contract_test.go
2// Verify the response format matches the OpenAPI spec
3
4package api_test
5
6import (
7 "encoding/json"
8 "net/http/httptest"
9 "strings"
10 "testing"
11
12 "github.com/getkin/kin-openapi/openapi3"
13 "github.com/stretchr/testify/assert"
14)
15
16func TestContractCompliance(t *testing.T) {
17 // Load the OpenAPI spec
18 loader := openapi3.NewLoader()
19 doc, err := loader.LoadFromFile("../../api/openapi/santekno-shop.yaml")
20 if err != nil {
21 t.Fatal(err)
22 }
23
24 if err := doc.Validate(loader.Context); err != nil {
25 t.Fatal("Invalid OpenAPI spec:", err)
26 }
27
28 // Test: POST /auth/register
29 t.Run("register returns 201 matching schema", func(t *testing.T) {
30 e := setupTestServer()
31 req := httptest.NewRequest("POST", "/api/v1/auth/register",
32 strings.NewReader(`{"email":"test@example.com","password":"Secret123","name":"Test"}`))
33 req.Header.Set("Content-Type", "application/json")
34 rec := httptest.NewRecorder()
35 e.ServeHTTP(rec, req)
36
37 // Verify status
38 assert.Equal(t, 201, rec.Code)
39
40 // Validate response against the OpenAPI schema
41 var body map[string]interface{}
42 json.NewDecoder(rec.Body).Decode(&body)
43
44 // body must have: id, email, name, created_at
45 assert.NotEmpty(t, body["id"])
46 assert.Equal(t, "test@example.com", body["email"])
47 assert.NotEmpty(t, body["created_at"])
48
49 // token MUST NOT be in the response (per spec + security)
50 assert.Nil(t, body["access_token"])
51 })
52}This test enforces a subtle requirement from the spec: register must not return a token (assert.Nil(t, body["access_token"])). Contract testing catches violations like this that easily slip past manual review.
03.10 OpenAPI in the CI Pipeline
The contract must be enforced automatically on every PR. The CI configuration below validates the spec and ensures the generated code is always in sync with the spec.
1# .github/workflows/ci.yml (add to the pipeline)
2
3- name: Validate OpenAPI spec
4 run: swagger-cli validate api/openapi/santekno-shop.yaml
5
6- name: Verify generated code is up-to-date
7 run: |
8 oapi-codegen --config api/openapi/gen-config.yaml api/openapi/santekno-shop.yaml
9 # Check if there's a diff (means the generated code wasn't updated)
10 if ! git diff --quiet internal/api/generated.go; then
11 echo "::error::Generated API code is out of sync with OpenAPI spec"
12 echo "Run: oapi-codegen --config api/openapi/gen-config.yaml ..."
13 exit 1
14 fiThe second step prevents the most common drift: the spec is updated but the generated code is forgotten to be regenerated. If there is a diff, CI immediately blocks the merge — the spec and the code are guaranteed to stay in sync.
03.11 TypeScript Types Generation
The frontend also needs type-safety from the same contract. The command below generates TypeScript types directly from the OpenAPI spec.
1# Generate TypeScript types for the frontend team
2openapi-typescript api/openapi/santekno-shop.yaml \
3 --output frontend/src/types/api.ts
4
5# Generated types:
6# export interface RegisterRequest {
7# email: string;
8# name: string;
9# password: string;
10# }
11#
12# export interface OrderResponse {
13# id: string; // UUID
14# status: "PENDING" | "CONFIRMED" | "SHIPPED" | "DELIVERED" | "CANCELLED";
15# total_amount_idr: number; // int64 cents
16# }
17
18# Frontend package.json script:
19# "gen:types": "openapi-typescript ../api/openapi/santekno-shop.yaml -o src/types/api.ts"With types generated from the same spec as the backend, the frontend and backend cannot drift apart: one spec change instantly updates both sides. End-to-end type-safety without duplicating definitions.
03.12 API Versioning Strategy
The API will evolve, and we need clear rules for changes. The guide below defines what counts as a breaking change and what the versioning strategy is.
1Santekno Shop API versioning:
2
3URL versioning: /api/v1/
4 The most explicit choice and easiest for a proxy/gateway
5
6Breaking change policy:
7 Non-breaking changes: can go into v1 directly
8 + Add an optional field to a response
9 + Add an optional query param
10 + Add a new endpoint
11
12 Breaking changes: require v2
13 - Remove a field from a response
14 - Change a field type
15 - Change the error format
16 - Rename an endpoint
17
18V2 strategy (when needed):
19 /api/v2/ for new endpoints
20 /api/v1/ still maintained for 6 months
21 Deprecation header in v1 responses
22
23For now: v1 only. V2 not needed at this time.Distinguishing breaking from non-breaking explicitly prevents surprises for API consumers. As long as a change only adds optional fields or new endpoints, old clients stay safe without needing a new version.
03.13 Webhook Spec
The payment webhook needs to be documented with its security requirements. The YAML spec below defines the webhook endpoint complete with the signature header and idempotent behavior.
1# Add to santekno-shop.yaml
2
3paths:
4 /webhooks/payment:
5 post:
6 tags: [Webhooks]
7 summary: Handle payment webhook
8 description: |
9 Receives payment status updates from the payment gateway.
10 Request must include an HMAC-SHA256 signature in the X-Signature header.
11 Idempotent: duplicate requests (same external_payment_id) return 200.
12 operationId: handlePaymentWebhook
13 parameters:
14 - name: X-Signature
15 in: header
16 required: true
17 schema: { type: string }
18 description: HMAC-SHA256 signature of the request body
19 requestBody:
20 required: true
21 content:
22 application/json:
23 schema:
24 type: object
25 required: [order_id, payment_id, status, amount_idr, signature]
26 properties:
27 order_id: { $ref: '#/components/schemas/UUID' }
28 payment_id: { type: string, example: "pay_xxx123" }
29 status:
30 type: string
31 enum: [PAID, FAILED]
32 amount_idr: { $ref: '#/components/schemas/AmountIDR' }
33 responses:
34 '200':
35 description: Webhook processed (including idempotent duplicates)
36 '401':
37 description: Invalid signature
38 content:
39 application/json:
40 schema: { $ref: '#/components/schemas/ErrorResponse' }
41 example: { error: "INVALID_SIGNATURE" }By documenting X-Signature as a required header and stating the idempotent behavior in the description, the security requirement becomes visible in the contract — not hidden in the code. The integrator team knows exactly what they must send.
03.14 API Documentation Website
The same contract can become interactive documentation. The commands below serve the spec as a documentation website via Redoc or Swagger UI.
1# Setup Swagger UI for API documentation
2# You can use redoc or swagger-ui
3
4# Option 1: Redoc (cleaner)
5docker run -p 8081:80 \
6 -v $(pwd)/api/openapi:/usr/share/nginx/html/openapi \
7 -e SPEC_URL=/openapi/santekno-shop.yaml \
8 redocly/redoc
9
10# Option 2: Swagger UI
11docker run -p 8081:8080 \
12 -e SWAGGER_JSON=/openapi/santekno-shop.yaml \
13 -v $(pwd)/api/openapi:/openapi \
14 swaggerapi/swagger-uiBecause the documentation is rendered from the same spec, it will never go stale: every contract update automatically refreshes the website. There is no more API documentation that differs from the actual behavior.
03.15 Spec Drift Prevention
To keep the spec, code, and tests always aligned, we combine the checks into a single script. The script below validates the spec, ensures the code is in sync, and runs the contract tests all at once.
1# Script to check spec vs implementation alignment
2# scripts/check-spec-drift.sh
3
4# 1. Validate that the spec is valid
5swagger-cli validate api/openapi/santekno-shop.yaml || exit 1
6
7# 2. Check the generated code is up-to-date
8oapi-codegen --config api/openapi/gen-config.yaml api/openapi/santekno-shop.yaml
9git diff --quiet internal/api/generated.go || {
10 echo "Generated code not in sync with spec"
11 exit 1
12}
13
14# 3. Run contract tests
15go test ./internal/api/... -run TestContractCompliance
16
17echo "Spec alignment check passed"A single script that combines these three checks becomes the sole definition of “spec aligned.” Run in pre-commit and CI, it closes the drift gap from three directions at once.
03.16 OpenAPI Extensions for Santekno Shop
OpenAPI allows custom metadata via x- extensions. The spec below links every endpoint back to the SDD spec and the ACs behind it.
1# Custom extensions (x-) for additional metadata
2paths:
3 /orders:
4 post:
5 x-spec-file: ".specify/features/place-order/spec.md" # link to the SDD spec
6 x-acceptance-criteria:
7 - AC1
8 - AC2
9 - AC3
10 - AC4
11 - AC5
12 x-implementation-status: "complete" # or "planned", "in-progress"
13 x-since-version: "1.0.0"This extension creates a traceability chain: business need → SDD spec → OpenAPI contract → implementation. During an audit, we can immediately tell which AC is the basis for an endpoint.
03.17 API Rate Limits Documentation
Rate limits need to be visible to API consumers. The spec below documents the rate limit headers on the login response, including Retry-After when the limit is exceeded.
1# Add rate limit info to the OpenAPI spec via headers
2paths:
3 /auth/login:
4 post:
5 responses:
6 '200':
7 headers:
8 X-RateLimit-Limit:
9 schema: { type: integer }
10 description: Max requests per window (5)
11 X-RateLimit-Remaining:
12 schema: { type: integer }
13 description: Remaining requests in the window
14 X-RateLimit-Reset:
15 schema: { type: integer }
16 description: Unix timestamp when the window resets
17 '429':
18 headers:
19 Retry-After:
20 schema: { type: integer }
21 description: Seconds to wait before retry (900)Documenting headers such as X-RateLimit-Remaining helps clients behave well: they can slow down requests before hitting a 429. The rate limit becomes a contract, not a surprise.
03.18 Pagination Standard
Pagination must be uniform across all list endpoints. The spec below defines reusable pagination parameters so there is no variation between endpoints.
1# Consistent pagination across all list endpoints
2
3components:
4 parameters:
5 PageParam:
6 name: page
7 in: query
8 schema: { type: integer, default: 1, minimum: 1 }
9 LimitParam:
10 name: limit
11 in: query
12 schema: { type: integer, default: 10, minimum: 1, maximum: 100 }
13
14# Usage:
15paths:
16 /orders:
17 get:
18 parameters:
19 - $ref: '#/components/parameters/PageParam'
20 - $ref: '#/components/parameters/LimitParam'
21 - name: status
22 in: query
23 schema: { $ref: '#/components/schemas/OrderStatus' }By defining PageParam and LimitParam once and reusing them via $ref, every list endpoint automatically has the same defaults and limits. Pagination consistency is maintained without duplication.
03.19 Changelog Spec
The API change history is best kept living inside the spec itself. The extension below stores a structured changelog inside the info block.
1# Maintain the changelog in the OpenAPI spec
2
3info:
4 title: Santekno Shop API
5 version: '1.0.0'
6 x-changelog:
7 - version: "1.0.0"
8 date: "2026-07-01"
9 changes:
10 - "Initial release"
11 - "Auth: register, login, refresh token"
12 - "Orders: place, cancel, list"
13 - "Products: list, search, detail"
14 - "Webhooks: payment callback"Keeping the changelog inside the spec means the version history is versioned together with the contract in git. There is no more separate changelog that is easy to forget to update.
03.20 Summary: Contract Before Code
The OpenAPI spec we built is the bridge between the SDD spec (the first article) and the Go implementation (the following articles).
What is available now:
- 4 tag groups: Auth, Orders, Products, Webhooks
- 10+ endpoints with complete request/response schemas
- Mock server ready for frontend development
- Code generation for Go types and interfaces
- TypeScript types for the frontend
- Contract testing skeleton
CI pipeline checks:
- Spec validation (swagger-cli)
- Generated code sync check
- Contract compliance tests
With a solid OpenAPI spec, the team can work in parallel: the frontend consumes the mock server, the backend implements according to the contract, and CI keeps both aligned. Every change in API behavior flows through this chain so that drift is detected at the spec level before it reaches production.
Next, we assemble the operational foundation: a comprehensive CLAUDE.md, the project constitution, and an initial CI pipeline that enforces all these rules automatically.