Skip to content
Santekno.com | Level Up Your Engineering Skills
EN
📖 0%
06 Nov 2026 · 22 min read ·Article 83 / 208
Go

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.

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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.

text
 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 finish

The 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.

bash
 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 --version

These 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.

yaml
  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.

yaml
  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.

yaml
 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.

bash
 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.

bash
 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.go

With the OpenAPI contract ready, the step below generates the Go server code directly from that specification.

bash
 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.

go
 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.

bash
 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 Conflict

With 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.

go
 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.

yaml
 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    fi

The 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.

bash
 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.

text
 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.

yaml
 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.

bash
 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-ui

Because 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.

bash
 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.

yaml
 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.

yaml
 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.

yaml
 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.

yaml
 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.

Related Articles

💬 Comments