Bagian 1 selesai dengan Notes API yang berdiri sendiri. Sekarang masuk Bagian 2: menerapkan workflow spec-first yang sama ke project yang strukturnya lebih dekat dengan kondisi kerja sehari-hari — boilerplate Go dengan hexagonal architecture (disebut juga ports & adapters).
Kenapa Hexagonal Architecture
Di hexagonal architecture, business logic (domain + use case) dipisah total dari detail teknis (HTTP framework, database, message broker) lewat port (interface) dan adapter (implementasi). HTTP handler adalah salah satu adapter — dan ini pas sekali dengan spec-first: ServerInterface hasil generate oapi-codegen adalah port di sisi inbound (HTTP), tinggal diimplementasikan oleh adapter handler.
Struktur Boilerplate
Project yang dipakai di bagian ini punya struktur seperti ini (ringkas):
internal/
├── domain/ # entity (User, dst) - murni business object
├── port/
│ ├── inbound/ # interface use case (dipanggil adapter HTTP/gRPC)
│ └── outbound/ # interface yang dibutuhkan use case (repository, cache)
├── app/ # implementasi use case (service)
├── adapter/
│ ├── inbound/http/
│ │ ├── generated/ # hasil oapi-codegen, per file spec
│ │ ├── handler/ # implement ServerInterface
│ │ └── router/ # daftarkan semua handler ke Fiber
│ └── outbound/pgsql/ # implementasi repository pakai GORM + PostgreSQL
├── infra/db/migrations/ # migration SQL (goose)
└── bootstrap/ # dependency injection container
Sudah ada fitur User yang lengkap mengikuti pola ini. Tujuan Bagian 2 adalah menambahkan fitur Product dari nol, lapis demi lapis, mengikuti pola yang persis sama — sehingga di akhir, siapa pun yang sudah paham cara User API dibangun bisa langsung paham cara Product API dibangun, karena konvensinya identik.
Membedah Konvensi Spec yang Sudah Ada
Sebelum menulis product-api.yaml, kita baca dulu user-api.yaml yang sudah ada, supaya spec baru konsisten gaya penulisannya:
info:
title: GoHexaClean User API
description: |
Production-ready User Management API built with Hexagonal + Clean Architecture.
version: 1.0.0
contact:
name: API Support
email: support@example.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: http://localhost:8080/api/v1
description: Local development server
Beberapa konvensi yang diikuti: base URL sudah termasuk prefix /api/v1, response selalu dibungkus {success, message, data}, error pakai schema ErrorResponse yang konsisten, dan tiap schema request/response punya example di tiap field (bukan cuma type) — supaya dokumentasi yang dihasilkan langsung enak dibaca tanpa perlu nebak-nebak bentuk nilainya.
Menulis product-api.yaml
Mengikuti pola yang sama, berikut spec Product API — CRUD sederhana tanpa fitur tambahan (import/export sengaja tidak dimasukkan, supaya fokus pada mekanisme spec-first-nya, bukan kompleksitas fitur):
openapi: 3.0.3
info:
title: GoHexaClean Product API
description: |
Product catalog API built with Hexagonal + Clean Architecture, added
on top of the GoHexaClean boilerplate using a spec-first OpenAPI workflow.
## Features
- Product CRUD operations
- Pagination support
- Generated server interface via oapi-codegen
version: 1.0.0
contact:
name: API Support
email: support@example.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: http://localhost:8080/api/v1
description: Local development server
security: []
tags:
- name: Products
description: Product catalog CRUD endpoints
paths:
/products:
get:
tags: [Products]
summary: List products
description: Get paginated list of products
operationId: listProducts
parameters:
- name: page
in: query
description: Page number
required: false
schema:
type: integer
minimum: 1
default: 1
- name: limit
in: query
description: Items per page
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 10
responses:
'200':
description: List of products
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedProductResponse'
post:
tags: [Products]
summary: Create product
operationId: createProduct
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProductRequest'
responses:
'201':
description: Product created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/ProductResponse'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/products/{id}:
get:
tags: [Products]
summary: Get product by ID
operationId: getProductById
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Product found
content:
application/json:
schema:
$ref: '#/components/schemas/ProductResponse'
'404':
description: Product not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
# put dan delete mengikuti pola yang sama persis dengan get,
# bedanya di requestBody (untuk put) dan response (SuccessResponse untuk delete)
components:
schemas:
CreateProductRequest:
type: object
required: [name, price, stock]
properties:
name:
type: string
minLength: 1
maxLength: 255
example: Kaos IndoKoding
price:
type: number
format: float
minimum: 0
example: 99000
stock:
type: integer
minimum: 0
example: 50
Product:
type: object
properties:
id:
type: string
format: uuid
example: 550e8400-e29b-41d4-a716-446655440000
name:
type: string
example: Kaos IndoKoding
price:
type: number
format: float
example: 99000
stock:
type: integer
example: 50
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
(Schema UpdateProductRequest, ProductResponse, PaginatedProductResponse, SuccessResponse, ErrorResponse mengikuti pola yang sama dengan user-api.yaml — dibungkus {success, message, data} / {success, message, data, meta}.)
Validasi
Sebelum lanjut generate kode, spec ini dilint dulu — kebiasaan yang sudah dibangun di Part 5:
$ npx @redocly/cli lint api/openapi/product-api.yaml
api/openapi/product-api.yaml: validated in 25ms
Woohoo! Your API description is valid. 🎉
You have 2 warnings.
Dua warning yang tersisa sama sifatnya dengan Notes API: server localhost (memang development) dan satu operation tanpa response 4xx — keduanya informasional, bukan blocking.
Spec product-api.yaml ini kemudian ditaruh di folder yang sama dengan user-api.yaml (api/openapi/). Script scripts/generate-openapi.sh yang sudah ada di boilerplate otomatis mendeteksi semua file YAML di folder itu — artinya kita tidak perlu mengubah apa pun di script generate, cukup tambah file baru.
Di Part 7, spec ini di-generate jadi kode, dan kita bangun seluruh lapisan hexagonal untuk Product: domain, port, service, repository, sampai handler yang meng-implement interface hasil generate.
Bagian dari Series: OpenAPI Spec-First untuk Go Developer
Seri 8 bagian: belajar mendesain REST API dengan pendekatan spec-first di Go - menulis kontrak OpenAPI dulu, generate kode, lalu terapkan ke project h...
Lihat Series Lengkap