Skip to content
Santekno.com | Level Up Your Engineering Skills
ID
📖 0%
24 Jul 2026 · 21 mnt baca ·Artikel 8 / 208
Go

OpenAPI Spec dengan Claude Code: Contract-First API Design untuk Golang

Panduan lengkap membuat OpenAPI specification dengan Claude Code menggunakan pendekatan contract-first. Dari feature spec ke OpenAPI YAML, generate Go server code, dan validasi contract antar service.

IH
Ihsan Arif
Penulis di Santekno · Backend Engineer

OpenAPI Spec Pertama dengan Claude: Contract-First API Design

Membuat OpenAPI spec golang dengan Claude Code adalah cara paling efektif untuk menegakkan disiplin contract-first di tim backend. Di banyak tim, API design justru terjadi secara implicit — developer menulis kode, endpoint muncul, dan dokumentasi dibuat setelah fakta. Ini adalah anti-pattern yang mahal: consumer service perlu menunggu implementasi selesai untuk tahu cara menggunakannya, perubahan bisa breaking tanpa notice, dan tidak ada single source of truth untuk API contract.

Contract-first API design membalikkan urutan ini: definisikan API contract lebih dulu dalam OpenAPI specification, review bersama stakeholder, baru implementasikan. Di artikel ini, kita pelajari bagaimana menggunakan Claude Code untuk menghasilkan OpenAPI spec yang production-grade dari feature spec yang sudah kita buat.


08.1 Mengapa Contract-First Lebih dari Sekadar Dokumentasi

Contract-first bukan hanya tentang dokumentasi yang bagus — ada keuntungan engineering yang konkret:

Paralel Development: Tim frontend dan backend bisa bekerja paralel. Frontend mock API dari contract, backend implementasi sesuai contract. Tidak perlu menunggu satu sama lain.

Explicit Breaking Change Detection: Ketika contract berubah, semua consumer langsung tahu — bukan melalui runtime error tapi melalui schema validation.

Testable Contract: Contract yang terdefinisi dalam OpenAPI bisa digunakan untuk automated contract testing antara producer dan consumer.

AI Code Generation yang Lebih Akurat: Dengan OpenAPI contract yang jelas, Claude Code bisa generate handler, middleware, dan test yang lebih tepat sasaran.


08.2 Dari Feature Spec ke OpenAPI: Alur Kerja

Sebelum masuk ke prompt dan kode, penting memahami peta besarnya. Diagram alur berikut menunjukkan bagaimana satu feature spec bercabang menjadi OpenAPI spec, kode server, dan contract test.

text
 1Feature Spec (specs/order/cancel-order.md)
 2 3         ▼ [Claude Code]
 4OpenAPI Spec (api/openapi.yaml)
 5 6         ├─► Go Server Code (internal/delivery/http/handler/)
 7         │   [oapi-codegen atau kin-openapi]
 8 9         └─► Contract Test (test/contract/)
10             [schemathesis atau dredd]

Yang perlu diperhatikan dari alur di atas: OpenAPI spec berada di tengah sebagai hub — kode server dan contract test sama-sama diturunkan darinya, sehingga keduanya dijamin konsisten dengan satu sumber kebenaran.


08.3 Prompt untuk Generate OpenAPI dari Feature Spec

Langkah pertama adalah mengubah feature spec menjadi OpenAPI spec. Prompt berikut memberi Claude aturan ketat agar setiap AC dan error case terpetakan ke endpoint dan response schema.

text
 1Berdasarkan feature spec berikut, hasilkan OpenAPI 3.0.3 specification
 2dalam format YAML.
 3
 4Requirements:
 51. Setiap AC yang menyebutkan HTTP endpoint harus ter-representasikan
 62. Setiap error case harus punya response schema dengan field error_code dan message
 73. Gunakan reusable components untuk schema yang dipakai di banyak tempat
 84. Include security scheme untuk JWT Bearer token
 95. Response schema harus konsisten dengan konvensi: { data: ... } untuk success,
10   { error_code: ..., message: ... } untuk error
116. Include example values yang realistic (bukan "string" tapi "ORD-20250715-ABC123")
12
13Tambahan konteks:
14- Base URL: /api/v1
15- Authentication: Bearer JWT (semua endpoint kecuali yang public)
16- Semua ID adalah UUID v4
17
18---
19@specs/order/cancel-order.md

Aturan nomor 6 tentang example yang realistic sering diremehkan padahal krusial: example seperti ORD-20250715-ABC123 membuat spec langsung berguna untuk mock server dan dokumentasi, sementara "string" tidak memberi informasi apa pun ke consumer.


08.4 Contoh OpenAPI Spec: Cancel Order Endpoint

Dari prompt di atas, Claude menghasilkan OpenAPI spec seperti berikut untuk endpoint cancel order — lengkap dengan semua response code dan reusable component.

yaml
  1# api/openapi.yaml (excerpt: cancel order endpoint)
  2openapi: "3.0.3"
  3info:
  4  title: Santekno Shop — Order Service API
  5  version: "1.0.0"
  6  description: API for managing orders in Santekno Shop
  7
  8servers:
  9  - url: https://api.santekno.com/v1
 10    description: Production
 11  - url: https://api-staging.santekno.com/v1
 12    description: Staging
 13  - url: http://localhost:8080/v1
 14    description: Local development
 15
 16security:
 17  - BearerAuth: []
 18
 19paths:
 20  /orders/{id}:
 21    delete:
 22      operationId: cancelOrder
 23      summary: Cancel an order
 24      description: |
 25        Cancels a PENDING order within the 15-minute cancellation window.
 26        Restores stock for all items atomically.
 27        Publishes ORDER_CANCELLED event to Kafka (best effort).
 28      tags:
 29        - orders
 30      parameters:
 31        - name: id
 32          in: path
 33          required: true
 34          description: Order UUID
 35          schema:
 36            type: string
 37            format: uuid
 38            example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
 39      responses:
 40        "204":
 41          description: Order successfully cancelled
 42        "401":
 43          description: Authentication required
 44          content:
 45            application/json:
 46              schema:
 47                $ref: "#/components/schemas/ErrorResponse"
 48              example:
 49                error_code: UNAUTHORIZED
 50                message: "authentication required"
 51        "404":
 52          description: Order not found
 53          content:
 54            application/json:
 55              schema:
 56                $ref: "#/components/schemas/ErrorResponse"
 57              example:
 58                error_code: ORDER_NOT_FOUND
 59                message: "order not found"
 60        "409":
 61          description: Order cannot be cancelled
 62          content:
 63            application/json:
 64              schema:
 65                $ref: "#/components/schemas/OrderNotCancellableError"
 66              examples:
 67                wrongStatus:
 68                  summary: Order already confirmed
 69                  value:
 70                    error_code: ORDER_NOT_CANCELLABLE
 71                    message: "order cannot be cancelled: current status is CONFIRMED"
 72                windowExpired:
 73                  summary: Cancellation window expired
 74                  value:
 75                    error_code: CANCEL_WINDOW_EXPIRED
 76                    message: "cancellation window has expired"
 77        "500":
 78          description: Internal server error
 79          content:
 80            application/json:
 81              schema:
 82                $ref: "#/components/schemas/ErrorResponse"
 83              example:
 84                error_code: INTERNAL_ERROR
 85                message: "internal server error"
 86
 87components:
 88  securitySchemes:
 89    BearerAuth:
 90      type: http
 91      scheme: bearer
 92      bearerFormat: JWT
 93
 94  schemas:
 95    ErrorResponse:
 96      type: object
 97      required:
 98        - error_code
 99        - message
100      properties:
101        error_code:
102          type: string
103          description: Machine-readable error code
104          example: "ORDER_NOT_FOUND"
105        message:
106          type: string
107          description: Human-readable error message
108          example: "order not found"
109
110    OrderNotCancellableError:
111      allOf:
112        - $ref: "#/components/schemas/ErrorResponse"
113      properties:
114        current_status:
115          type: string
116          enum:
117            - CONFIRMED
118            - SHIPPED
119            - DELIVERED
120            - CANCELLED
121          description: Current order status
122          example: "CONFIRMED"

Perhatikan bagaimana response 409 punya dua contoh berbeda (wrongStatus dan windowExpired) untuk satu status code — ini memetakan langsung ke AC9 dan AC10 di feature spec, membuktikan setiap keputusan bisnis punya jejak di contract.


08.5 Validasi OpenAPI Spec dengan Claude

Setelah spec di-generate, jangan langsung dipakai — minta Claude mereviewnya terhadap feature spec asli. Prompt berikut memastikan tidak ada AC yang tercecer sebelum kode di-generate.

text
 1Review OpenAPI spec berikut dan identifikasi:
 2
 31. Apakah semua AC dari feature spec sudah ter-representasikan?
 4   (sertakan feature spec untuk comparison)
 52. Apakah response schema sudah lengkap dan consistent?
 63. Apakah ada missing examples yang bisa membuat spec lebih berguna?
 74. Apakah naming sudah konsisten dengan konvensi API yang ada?
 85. Apakah ada security concern yang perlu ditambahkan?
 96. Apakah spec ini bisa langsung dipakai untuk generate server code?
10
11Feature spec: @specs/order/cancel-order.md
12OpenAPI spec: @api/openapi.yaml

Dengan menyertakan feature spec sebagai pembanding, review ini menutup celah paling umum di contract-first: endpoint yang terlihat lengkap tapi diam-diam melewatkan satu error case yang ada di spec bisnis.


08.6 Menghasilkan Go Server Code dari OpenAPI

Begitu spec di-approve, kita bisa generate kode Go langsung dari OpenAPI menggunakan oapi-codegen. Perintah berikut memasang tool-nya.

bash
1go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest

Setelah tool terpasang, kita perlu file konfigurasi yang menentukan apa saja yang akan di-generate. Konfigurasi berikut meminta types, server, dan spec sekaligus.

yaml
1# api/codegen-config.yaml
2package: api
3generate:
4  - types
5  - server
6  - spec
7output: internal/delivery/http/api/api.gen.go

Dengan konfigurasi tersimpan, generate kode cukup satu perintah yang menunjuk ke config dan file OpenAPI.

bash
1oapi-codegen -config api/codegen-config.yaml api/openapi.yaml

Hasilnya adalah kode Go yang berisi tipe parameter dan interface server yang harus kamu implementasikan, seperti cuplikan berikut.

go
 1// internal/delivery/http/api/api.gen.go (generated — jangan edit manual)
 2package api
 3
 4// CancelOrderParams defines parameters for CancelOrder
 5type CancelOrderParams struct {
 6    // ID Order UUID
 7    ID openapi_types.UUID `json:"id"`
 8}
 9
10// StrictServerInterface represents all server handlers
11type StrictServerInterface interface {
12    // CancelOrder cancels a pending order
13    // (DELETE /orders/{id})
14    CancelOrder(ctx context.Context, request CancelOrderRequestObject) (CancelOrderResponseObject, error)
15}

Kunci dari output ini adalah StrictServerInterface: karena handler kamu wajib memenuhi interface ini, contract ditegakkan pada level compile-time — jika endpoint di spec berubah, kode yang tidak sinkron langsung gagal build.


08.7 Implementasi Handler dari Generated Interface

Setelah interface tergenerate, tugas kita adalah mengimplementasikannya dengan menghubungkan usecase dan memetakan error domain ke response OpenAPI. Handler berikut menunjukkan pemetaan lengkapnya.

go
 1// internal/delivery/http/handler/order_handler.go
 2package handler
 3
 4import (
 5    "context"
 6    "errors"
 7    "log/slog"
 8
 9    "github.com/santekno/santekno-shop/internal/delivery/http/api"
10    "github.com/santekno/santekno-shop/internal/domain/order"
11    cancelorder "github.com/santekno/santekno-shop/internal/usecase/order/cancel_order"
12)
13
14type OrderHandler struct {
15    cancelOrderUC *cancelorder.CancelOrderUseCase
16}
17
18func NewOrderHandler(cancelOrderUC *cancelorder.CancelOrderUseCase) *OrderHandler {
19    return &OrderHandler{
20        cancelOrderUC: cancelOrderUC,
21    }
22}
23
24// CancelOrder implements api.StrictServerInterface
25// Implements specs/order/cancel-order.md v1.3
26func (h *OrderHandler) CancelOrder(
27    ctx context.Context,
28    request api.CancelOrderRequestObject,
29) (api.CancelOrderResponseObject, error) {
30
31    userID := ctx.Value(userIDKey{}).(uuid.UUID) // dari auth middleware
32
33    input := cancelorder.CancelOrderInput{
34        OrderID: uuid.UUID(request.Id),
35        UserID:  userID,
36    }
37
38    err := h.cancelOrderUC.Execute(ctx, input)
39    if err != nil {
40        return h.mapCancelOrderError(err)
41    }
42
43    // AC7: 204 No Content
44    return api.CancelOrder204Response{}, nil
45}
46
47// mapCancelOrderError maps domain errors ke OpenAPI response types
48func (h *OrderHandler) mapCancelOrderError(err error) (api.CancelOrderResponseObject, error) {
49    var notCancellableErr *cancelorder.OrderNotCancellableError
50
51    switch {
52    // AC8: Order not found atau bukan milik user → 404
53    case errors.Is(err, cancelorder.ErrOrderNotFound):
54        return api.CancelOrder404JSONResponse{
55            ErrorCode: "ORDER_NOT_FOUND",
56            Message:   "order not found",
57        }, nil
58
59    // AC9: Status bukan PENDING → 409
60    case errors.As(err, &notCancellableErr):
61        return api.CancelOrder409JSONResponse{
62            ErrorCode:     "ORDER_NOT_CANCELLABLE",
63            Message:       fmt.Sprintf("order cannot be cancelled: current status is %s", notCancellableErr.CurrentStatus),
64            CurrentStatus: (*api.OrderNotCancellableErrorCurrentStatus)(&notCancellableErr.CurrentStatus),
65        }, nil
66
67    // AC10: Window expired → 409
68    case errors.Is(err, cancelorder.ErrCancelWindowExpired):
69        return api.CancelOrder409JSONResponse{
70            ErrorCode: "CANCEL_WINDOW_EXPIRED",
71            Message:   "cancellation window has expired",
72        }, nil
73
74    // Unexpected error → 500
75    default:
76        slog.ErrorContext(ctx, "cancel order: unexpected error", "error", err)
77        return api.CancelOrder500JSONResponse{
78            ErrorCode: "INTERNAL_ERROR",
79            Message:   "internal server error",
80        }, nil
81    }
82}

Setiap cabang di mapCancelOrderError diberi komentar AC yang sesuai, dan tipe response seperti CancelOrder409JSONResponse berasal dari kode generated — artinya jika kamu salah mengembalikan tipe, compiler menolak. Inilah cara contract-first membuat drift antara spec dan implementasi mustahil lolos diam-diam.


08.8 Contract Testing dengan OpenAPI

Salah satu keuntungan terbesar contract-first adalah kemampuan menjalankan automated contract testing. Pendekatan pertama memakai schemathesis untuk memvalidasi server berjalan terhadap spec.

bash
1# Install schemathesis
2pip install schemathesis
3
4# Jalankan contract test terhadap running server
5schemathesis run api/openapi.yaml \
6    --base-url http://localhost:8080/v1 \
7    --auth "Bearer $TEST_JWT_TOKEN" \
8    --endpoint "/orders/{id}" \
9    --method DELETE

schemathesis otomatis menurunkan test case dari spec, jadi ia menangkap ketidaksesuaian response tanpa kamu menulis assertion manual. Pendekatan kedua adalah consumer-driven contract testing dengan Pact, yang memverifikasi kesepakatan antara producer dan consumer.

go
 1// test/contract/order_cancel_pact_test.go
 2package contract_test
 3
 4import (
 5    "testing"
 6    "github.com/pact-foundation/pact-go/v2/consumer"
 7)
 8
 9func TestCancelOrderContract(t *testing.T) {
10    pact, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{
11        Consumer: "NotificationService",
12        Provider: "OrderService",
13    })
14    if err != nil {
15        t.Fatal(err)
16    }
17
18    // Define the expected interaction
19    pact.
20        AddInteraction().
21        Given("order f47ac10b exists and is PENDING").
22        UponReceiving("a cancel order request").
23        WithRequest(consumer.Request{
24            Method: "DELETE",
25            Path:   "/v1/orders/f47ac10b-58cc-4372-a567-0e02b2c3d479",
26            Headers: consumer.MapMatcher{
27                "Authorization": consumer.Like("Bearer eyJhbGci..."),
28            },
29        }).
30        WillRespondWith(consumer.Response{
31            Status: 204,
32        })
33
34    // Execute the test
35    err = pact.ExecuteTest(t, func(config consumer.MockServerConfig) error {
36        // Call our client
37        client := NewOrderClient(config.Host, config.Port)
38        return client.CancelOrder("f47ac10b-58cc-4372-a567-0e02b2c3d479")
39    })
40    if err != nil {
41        t.Fatal(err)
42    }
43}

Dengan Pact, consumer (Notification Service) dan producer (Order Service) bisa diuji kesepakatannya secara independen tanpa integration test penuh — sangat berharga di arsitektur microservice di mana kedua sisi dikembangkan tim berbeda.


08.9 OpenAPI untuk Multiple Endpoints: Complete Create Order Spec

Cancel order hanya satu endpoint; API nyata punya banyak. Contoh berikut memperlihatkan spec yang lebih lengkap dengan create, get, dan cancel order sekaligus reusable components.

yaml
  1# api/openapi.yaml — lebih lengkap
  2paths:
  3  /orders:
  4    post:
  5      operationId: createOrder
  6      summary: Create a new order
  7      tags: [orders]
  8      requestBody:
  9        required: true
 10        content:
 11          application/json:
 12            schema:
 13              $ref: "#/components/schemas/CreateOrderRequest"
 14            example:
 15              cart_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
 16              shipping_address_id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
 17      responses:
 18        "201":
 19          description: Order created successfully
 20          content:
 21            application/json:
 22              schema:
 23                $ref: "#/components/schemas/CreateOrderResponse"
 24        "400":
 25          $ref: "#/components/responses/BadRequest"
 26        "401":
 27          $ref: "#/components/responses/Unauthorized"
 28        "422":
 29          description: Unprocessable entity
 30          content:
 31            application/json:
 32              schema:
 33                $ref: "#/components/schemas/ValidationError"
 34
 35  /orders/{id}:
 36    get:
 37      operationId: getOrder
 38      summary: Get order by ID
 39      tags: [orders]
 40      parameters:
 41        - $ref: "#/components/parameters/OrderID"
 42      responses:
 43        "200":
 44          description: Order details
 45          content:
 46            application/json:
 47              schema:
 48                $ref: "#/components/schemas/OrderResponse"
 49        "401":
 50          $ref: "#/components/responses/Unauthorized"
 51        "404":
 52          $ref: "#/components/responses/NotFound"
 53
 54    delete:
 55      operationId: cancelOrder
 56      # ... (seperti di atas)
 57
 58components:
 59  responses:
 60    BadRequest:
 61      description: Bad request
 62      content:
 63        application/json:
 64          schema:
 65            $ref: "#/components/schemas/ErrorResponse"
 66
 67    Unauthorized:
 68      description: Authentication required
 69      content:
 70        application/json:
 71          schema:
 72            $ref: "#/components/schemas/ErrorResponse"
 73          example:
 74            error_code: UNAUTHORIZED
 75            message: "authentication required"
 76
 77    NotFound:
 78      description: Resource not found
 79      content:
 80        application/json:
 81          schema:
 82            $ref: "#/components/schemas/ErrorResponse"
 83
 84  parameters:
 85    OrderID:
 86      name: id
 87      in: path
 88      required: true
 89      schema:
 90        type: string
 91        format: uuid
 92      example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
 93
 94  schemas:
 95    CreateOrderRequest:
 96      type: object
 97      required: [cart_id, shipping_address_id]
 98      properties:
 99        cart_id:
100          type: string
101          format: uuid
102          description: ID of the cart to create order from
103        shipping_address_id:
104          type: string
105          format: uuid
106          description: ID of the shipping address
107
108    CreateOrderResponse:
109      type: object
110      required: [order_id, order_number, total_amount_cents, status]
111      properties:
112        order_id:
113          type: string
114          format: uuid
115          example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
116        order_number:
117          type: string
118          example: "ORD-20250715-ABC1234"
119        total_amount_cents:
120          type: integer
121          format: int64
122          description: Total order amount in cents (IDR)
123          example: 1500000
124        status:
125          type: string
126          enum: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
127          example: PENDING
128        estimated_delivery:
129          type: string
130          format: date
131          description: Estimated delivery date (ISO 8601)
132          example: "2025-07-20"
133
134    OrderResponse:
135      type: object
136      required: [order_id, order_number, status, total_amount_cents, items, created_at]
137      properties:
138        order_id:
139          type: string
140          format: uuid
141        order_number:
142          type: string
143        status:
144          type: string
145          enum: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
146        total_amount_cents:
147          type: integer
148          format: int64
149        items:
150          type: array
151          items:
152            $ref: "#/components/schemas/OrderItem"
153        created_at:
154          type: string
155          format: date-time
156
157    OrderItem:
158      type: object
159      required: [product_id, product_name, quantity, price_cents]
160      properties:
161        product_id:
162          type: string
163          format: uuid
164        product_name:
165          type: string
166          example: "Laptop Gaming ASUS ROG"
167        quantity:
168          type: integer
169          minimum: 1
170        price_cents:
171          type: integer
172          format: int64
173          description: Price per unit in cents at time of order
174
175    ValidationError:
176      allOf:
177        - $ref: "#/components/schemas/ErrorResponse"
178      properties:
179        field:
180          type: string
181          description: Field that failed validation
182          example: "cart_id"

Perhatikan pemakaian $ref yang agresif — OrderID, Unauthorized, dan ErrorResponse didefinisikan sekali lalu dipakai ulang di banyak endpoint. Pola ini menjaga konsistensi respons di seluruh API dan membuat perubahan cukup dilakukan di satu tempat.


08.10 Mengintegrasikan OpenAPI dengan Swagger UI

Untuk internal development, ada baiknya menyajikan spec lewat Swagger UI agar tim bisa mengeksplorasi API secara interaktif. Kode main.go berikut menyajikan spec dan UI-nya.

go
 1// cmd/api/main.go
 2package main
 3
 4import (
 5    "github.com/labstack/echo/v4"
 6    echoSwagger "github.com/swaggo/echo-swagger"
 7)
 8
 9func main() {
10    e := echo.New()
11
12    // Serve OpenAPI spec
13    e.Static("/api-docs", "api")
14
15    // Swagger UI
16    e.GET("/swagger/*", echoSwagger.WrapHandler)
17
18    // API routes
19    api.RegisterHandlers(e, handler)
20
21    e.Start(":8080")
22}

Dengan Swagger UI aktif, consumer team bisa mencoba endpoint langsung dari browser sebelum menulis satu baris kode integrasi — mempercepat feedback loop yang jadi inti dari paralel development.


08.11 Versioning OpenAPI Spec

API contract yang serius butuh strategi versioning yang jelas agar perubahan bisa dikelola tanpa mengejutkan consumer. Struktur direktori berikut memisahkan versi current, upcoming, dan deprecated.

text
1api/
2├── openapi.yaml           ← current (v1)
3├── openapi-v2.yaml        ← upcoming version (draft)
4└── deprecated/
5    └── openapi-v0.yaml    ← deprecated, will be removed 2026-01-01

Pemisahan direktori ini membuat status setiap versi terlihat sekilas. Untuk penomorannya, terapkan semantic versioning di dalam file spec seperti berikut.

yaml
1# Dalam openapi.yaml
2info:
3  version: "1.3.0"
4  # MAJOR: breaking change (field dihapus, endpoint path berubah)
5  # MINOR: backward compatible addition (field baru ditambah)
6  # PATCH: clarification, documentation update, no schema change

Konvensi MAJOR/MINOR/PATCH ini memberi consumer sinyal instan tentang risiko sebuah update. Agar penilaian breaking atau tidak tidak bergantung pada perasaan, mintalah Claude membandingkan dua versi dengan prompt berikut.

text
1Bandingkan dua versi OpenAPI spec berikut dan identifikasi:
21. Breaking changes: perubahan yang akan break existing consumers
3   (field dihapus, type berubah, endpoint path berubah)
42. Non-breaking additions: tambahan yang backward-compatible
53. Deprecations: field atau endpoint yang harus di-deprecated
6
7Versi lama: @api/openapi-v1.yaml
8Versi baru: @api/openapi-v2.yaml

Dengan analisis terstruktur ini, keputusan menaikkan MAJOR atau MINOR jadi objektif — kamu punya daftar konkret perubahan breaking, bukan sekadar dugaan.


08.12 OpenAPI untuk Microservice API Contract

Ketika dua service berkomunikasi, OpenAPI bisa berfungsi sebagai kontrak eksplisit di antara mereka. Struktur berikut memisahkan full spec dari subset yang dikonsumsi service lain.

text
1api/
2├── openapi.yaml                           ← full API spec
3└── contracts/
4    └── consumed-by-notification-service.yaml  ← subset yang dipakai Notification Service

Memisahkan contract per-consumer membuat batas dependency menjadi eksplisit. Isi dari file contract tersebut sebaiknya mencantumkan owner dan aturan koordinasi perubahan, seperti berikut.

yaml
 1# api/contracts/consumed-by-notification-service.yaml
 2# Contract antara Order Service (producer) dan Notification Service (consumer)
 3# Created: 2025-07-15
 4# Owner: @order-service-team
 5# Consumer contact: @notification-service-team
 6
 7openapi: "3.0.3"
 8info:
 9  title: Order Service — Events consumed by Notification Service
10  description: |
11    This document describes the Kafka events published by Order Service
12    that are consumed by Notification Service.
13
14    Any changes to these events MUST be coordinated with @notification-service-team
15    and follow the breaking change process in ADR-008.

Dengan mencantumkan owner dan referensi ke ADR-008 langsung di dalam contract, perubahan tak lagi bisa dilakukan sepihak — dokumentasi dependency menjadi mekanisme koordinasi, bukan sekadar catatan.


08.13 Prompting Claude untuk Review API Design

Selain memvalidasi kelengkapan, Claude bisa berperan sebagai API design reviewer yang menilai kualitas desain. Prompt berikut mengarahkannya menilai empat dimensi desain RESTful sekaligus.

text
 1Kamu adalah senior API designer yang berpengalaman dengan RESTful API design
 2dan OpenAPI specification.
 3
 4Review API design berikut dan berikan feedback tentang:
 5
 61. RESTful design principles:
 7   - Apakah endpoint naming sudah sesuai resource-oriented design?
 8   - Apakah HTTP methods digunakan dengan benar?
 9   - Apakah HTTP status codes appropriate?
10
112. Schema design:
12   - Apakah response schema consistent di semua endpoint?
13   - Apakah ada redundant schema yang bisa di-reuse?
14   - Apakah naming convention consistent (camelCase vs snake_case)?
15
163. Security:
17   - Apakah semua endpoint yang perlu autentikasi sudah di-secure?
18   - Apakah ada potential information disclosure di error responses?
19
204. Developer experience:
21   - Apakah examples cukup untuk dipahami?
22   - Apakah descriptions jelas dan actionable?
23
24OpenAPI spec: @api/openapi.yaml

Review desain semacam ini menangkap masalah yang tidak terlihat oleh validator otomatis — misalnya endpoint yang secara teknis valid tapi melanggar prinsip resource-oriented, atau error response yang tanpa sadar membocorkan informasi sensitif.


08.14 Automating OpenAPI Validation di CI/CD

Validasi contract sebaiknya berjalan otomatis di setiap perubahan spec, bukan mengandalkan kedisiplinan manual. Workflow GitHub Actions berikut memvalidasi spec dan mendeteksi breaking change pada setiap pull request.

yaml
 1# .github/workflows/api-validation.yml
 2name: API Contract Validation
 3
 4on:
 5  pull_request:
 6    paths:
 7      - 'api/openapi.yaml'
 8
 9jobs:
10  validate-spec:
11    runs-on: ubuntu-latest
12    steps:
13      - uses: actions/checkout@v4
14
15      - name: Validate OpenAPI spec
16        uses: actions/setup-node@v4
17        with:
18          node-version: '20'
19
20      - run: npm install -g @apidevtools/swagger-parser
21
22      - name: Lint OpenAPI
23        run: swagger-parser validate api/openapi.yaml
24
25      - name: Check for breaking changes
26        uses: oasdiff/oasdiff-action@main
27        with:
28          base: refs/heads/main
29          revision: refs/heads/${{ github.head_ref }}
30
31      - name: Run contract tests
32        run: |
33          go test ./test/contract/... -v

Karena workflow ini hanya terpicu saat api/openapi.yaml berubah, ia menjadi quality gate yang murah namun efektif: breaking change tertangkap di PR, jauh sebelum sampai ke consumer di production.


08.15 Menggunakan Claude untuk Generate Test Cases dari OpenAPI

OpenAPI spec juga bisa jadi sumber untuk menghasilkan test case yang komprehensif. Prompt berikut meminta Claude membuat test Go yang mencakup semua response code dan edge case dari spec.

text
 1Berdasarkan OpenAPI spec untuk endpoint DELETE /orders/{id},
 2generate test cases yang comprehensive dalam Go menggunakan
 3testify/suite dan gomock.
 4
 5Test cases harus mencakup:
 61. Semua response codes yang didefinisikan di spec (204, 401, 404, 409, 500)
 72. Edge cases yang implied dari spec (concurrent requests, expired window)
 83. Schema validation — response body harus match schema yang didefinisikan
 9
10Gunakan pattern yang sama dengan: internal/usecase/order/create_order_test.go

Menyuruh Claude mengikuti pattern dari test yang sudah ada memastikan test baru konsisten dengan konvensi proyek — sekaligus memanfaatkan spec sebagai daftar lengkap kasus yang wajib diuji, sehingga tidak ada response code yang terlewat.


08.16 OpenAPI sebagai Sumber untuk API Documentation

Satu OpenAPI spec bisa langsung diubah menjadi berbagai format dokumentasi tanpa menulis ulang. Perintah berikut menghasilkan HTML docs, Postman collection, dan contoh curl sekaligus.

bash
 1# Generate HTML docs dengan Redoc
 2npx redoc-cli bundle api/openapi.yaml -o docs/api.html
 3
 4# Generate Postman collection
 5npx openapi-to-postman \
 6    --spec api/openapi.yaml \
 7    --output docs/postman-collection.json
 8
 9# Generate dari Claude
10claude "Generate contoh curl commands untuk semua endpoints di api/openapi.yaml
11       Sertakan contoh request body dan expected response untuk masing-masing"

Karena semua format ini diturunkan dari satu spec, dokumentasi otomatis selalu sinkron dengan contract — mengakhiri masalah klasik dokumentasi API yang basi begitu kode berubah.


08.17 Troubleshooting: Common Issues dengan OpenAPI di Go

Saat mengintegrasikan OpenAPI dengan Go, ada beberapa masalah yang berulang. Masalah pertama adalah tipe UUID yang tidak dikenali generator.

yaml
1# Solusi: gunakan format uuid
2properties:
3  id:
4    type: string
5    format: uuid  ← ini yang penting, bukan hanya "type: string"

Menambahkan format: uuid membuat generator menghasilkan tipe UUID yang benar alih-alih string mentah. Masalah kedua adalah overflow int64 saat JSON diparse oleh JavaScript.

yaml
1# Golang int64 bisa overflow jika JavaScript parse sebagai number
2# Solusi: gunakan x-go-type atau string untuk amount yang sangat besar
3properties:
4  amount_cents:
5    type: integer
6    format: int64
7    x-go-type: int64  ← explicit Go type

Anotasi x-go-type memaksa tipe Go yang eksplisit sehingga nilai besar tidak kehilangan presisi. Masalah ketiga adalah kebutuhan discriminator untuk response polymorphic.

yaml
 1# Untuk berbagai tipe error response dengan schema berbeda
 2components:
 3  schemas:
 4    OrderError:
 5      oneOf:
 6        - $ref: "#/components/schemas/OrderNotCancellableError"
 7        - $ref: "#/components/schemas/CancelWindowExpiredError"
 8      discriminator:
 9        propertyName: error_code
10        mapping:
11          ORDER_NOT_CANCELLABLE: "#/components/schemas/OrderNotCancellableError"
12          CANCEL_WINDOW_EXPIRED: "#/components/schemas/CancelWindowExpiredError"

Dengan discriminator yang memetakan error_code ke schema spesifik, tooling bisa memilih tipe yang tepat secara otomatis — memungkinkan satu field error menghasilkan beberapa bentuk response yang tetap type-safe.


08.18 Tips & Gotchas

💡 Tip 1: Mulai dengan spec minimal, tambahkan detail secara iteratif — spec OpenAPI yang terlalu lengkap di awal bisa jadi bottleneck. Mulai dengan endpoint, request/response schema, dan status codes, lalu tambahkan examples dan refinements secara iteratif.

💡 Tip 2: Gunakan $ref secara agresif untuk consistency — jika schema yang sama muncul di beberapa endpoint, selalu ekstrak ke components/schemas. Ini memastikan consistency dan membuat perubahan lebih mudah.

💡 Tip 3: Review spec bersama consumer team sebelum implementasi — tujuan contract-first adalah paralel development, dan ini hanya terjadi jika consumer team sudah review dan approve contract sebelum implementasi dimulai.

💡 Tip 4: Version spec di git dan track changes dengan commit messages yang jelas. Contoh commit message berikut menjelaskan sifat perubahan secara eksplisit.

bash
1git commit -m "api: add CANCEL_WINDOW_EXPIRED response to DELETE /orders/{id}
2
3Adds new 409 response variant for when cancellation window is expired.
4This is a non-breaking addition (new error code, existing consumers
5not affected if they handle 409 generically).
6
7Related to: specs/order/cancel-order.md v1.3"

Commit message yang menyebut eksplisit “non-breaking addition” dan mereferensikan spec membuat git history menjadi audit trail evolusi API — memudahkan menelusuri kapan dan kenapa sebuah response ditambahkan.

⚠️ Gotcha 1: Generated code tidak boleh diedit manual — file yang di-generate oapi-codegen harus ditandai // Code generated. DO NOT EDIT. dan tidak boleh diedit manual. Edit spec, generate ulang.

⚠️ Gotcha 2: OpenAPI spec tidak bisa express semua business rules — OpenAPI bagus untuk HTTP contract (input/output, status codes), tapi business rule kompleks seperti “cancel window 15 menit” tidak bisa di-express di OpenAPI — tetap perlu feature spec terpisah.

⚠️ Gotcha 3: Hindari circular references di schema — OpenAPI tidak support circular schema reference dengan baik di semua tools. Desain schema secara flat.

⚠️ Gotcha 4: Examples harus realistic, bukan placeholderexample: "string" atau example: 0 tidak berguna. Berikan contoh realistis seperti example: "ORD-20250715-ABC1234" atau example: 1500000.


08.19 Spec Governance: Siapa yang Approve OpenAPI Changes

Di proyek dengan banyak tim, perubahan OpenAPI butuh proses governance yang jelas agar tidak ada perubahan yang menabrak consumer tanpa persetujuan. Dokumen proses berikut memisahkan alur untuk perubahan non-breaking, breaking, dan deprecation.

markdown
 1# API Contract Change Process
 2
 3## Non-breaking changes (tambah field, endpoint baru)
 41. Developer buat PR dengan perubahan openapi.yaml
 52. Review oleh: API Lead + Consumer Team Lead
 63. Approval: 1 dari API Lead + 1 dari Consumer Team
 74. Merge: bisa langsung setelah approval
 8
 9## Breaking changes (hapus field, ubah type, ubah path)
101. RFC (Request for Change) harus dibuat minimal 2 sprint sebelumnya
112. Review period: 1 minggu untuk semua consumer team
123. Deprecation notice: field/endpoint lama deprecated selama 1 sprint
134. Actual removal: setelah semua consumer sudah migrate
145. Approval: API Lead + semua Consumer Team Lead
15
16## Endpoint deprecation
17Tambahkan x-deprecated: true dan deadline di spec:
18deprecated: true
19x-deprecation-notice: "Will be removed in API v2 on 2026-01-01"

Perbedaan perlakuan antara non-breaking dan breaking change di dokumen ini adalah inti governance-nya: perubahan berisiko rendah bisa cepat di-merge, sementara breaking change dipaksa melewati RFC dan deprecation period yang melindungi consumer.


08.20 Ringkasan

Contract-first API design menggunakan OpenAPI specification memberikan keuntungan konkret: paralel development, explicit breaking change detection, testable contract, dan AI code generation yang lebih akurat.

Alur kerja yang kita bangun: Feature spec → OpenAPI spec (via Claude) → Generated Go code → Contract tests → API documentation

Tools yang digunakan:

  • oapi-codegen untuk generate Go interface dari OpenAPI
  • schemathesis untuk automated contract testing
  • redoc-cli untuk generate human-readable documentation

Governance yang penting:

  • OpenAPI spec di git, versioned dengan semantic versioning
  • Breaking changes butuh RFC dan deprecation period
  • Consumer team harus review contract sebelum implementasi dimulai

Di artikel berikutnya, kita akan membahas dimensi spec yang sering terlewat: Spesifikasi Non-Fungsional — performance, security, scalability yang harus ada di spec sebelum implementasi.

Artikel Terkait

💬 Komentar