OpenAPI — kontrak API yang dijaga compiler
Dokumentasi API yang ditulis terpisah selalu basi. Pendekatan spec-first membalikkannya: spesifikasi jadi sumber kebenaran, dan compiler yang memastikan kodemu mematuhinya.
Intisari
- Spec-first: tulis
openapi.yaml→ generate tipe + interface server → compiler menolak kalau ada endpoint yang tidak diimplementasikan. - Alternatifnya code-first (anotasi komentar) — lebih cepat dimulai, tapi dokumentasinya kembali bisa basi.
- Klien untuk konsumen API bisa digenerate dalam banyak bahasa dari spesifikasi yang sama.
- Validasi permintaan bisa dijalankan otomatis dari spesifikasi lewat middleware.
- Untuk API internal antar tim, ini menghapus seluruh kategori kesalahpahaman kontrak.
Spesifikasinya
openapi: 3.0.3
info:
title: Toko API
version: 1.0.0
paths:
/produk:
get:
operationId: daftarProduk
parameters:
- name: q
in: query
schema: { type: string, maxLength: 100 }
- name: batas
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
responses:
"200":
description: Daftar produk
content:
application/json:
schema:
$ref: "#/components/schemas/HalamanProduk"
post:
operationId: buatProduk
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/BuatProdukReq"
responses:
"201": { description: Dibuat }
"422": { $ref: "#/components/responses/Validasi" }
components:
schemas:
Produk:
type: object
required: [id, nama, harga]
properties:
id: { type: integer, format: int64 }
nama: { type: string, minLength: 3, maxLength: 200 }
harga: { type: integer, format: int64, minimum: 0 }
Menghasilkan kode
# oapi-codegen.yaml
package: api
output: internal/api/api.gen.go
generate:
models: true
chi-server: true
strict-server: true
embedded-spec: true
go get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
go tool oapi-codegen -config oapi-codegen.yaml openapi.yaml
// internal/api/api.gen.go — DIGENERATE
type ServerInterface interface {
DaftarProduk(w http.ResponseWriter, r *http.Request, params DaftarProdukParams)
BuatProduk(w http.ResponseWriter, r *http.Request)
}
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler
Inilah bagian yang membuat pendekatan ini berbeda dari dokumentasi biasa. Karena
ServerInterface digenerate dari spesifikasi, menambahkan endpoint di
openapi.yaml membuat kodemu gagal kompilasi sampai kamu mengimplementasikannya. Dan
mengubah bentuk respons di spesifikasi membuat setiap handler yang masih mengembalikan bentuk lama ikut
gagal. Dokumentasi dan kode tidak bisa lagi berbeda.
Mengimplementasikannya
type Server struct {
produk *produk.Layanan
log *slog.Logger
}
var _ api.ServerInterface = (*Server)(nil) // pemeriksaan saat kompilasi (Fase 1)
func (s *Server) DaftarProduk(w http.ResponseWriter, r *http.Request,
p api.DaftarProdukParams) {
batas := 20
if p.Batas != nil {
batas = *p.Batas // sudah divalidasi 1–100 oleh middleware spec
}
hasil, err := s.produk.Cari(r.Context(), deref(p.Q), batas)
if err != nil {
s.tulisError(w, r, err)
return
}
tulisJSON(w, http.StatusOK, keHalaman(hasil))
}
Validasi otomatis dari spesifikasi
spec, _ := api.GetSwagger()
spec.Servers = nil // jangan validasi host
r := chi.NewRouter()
r.Use(nethttpmiddleware.OapiRequestValidator(spec))
h := api.HandlerFromMux(server, r)
Permintaan yang melanggar spesifikasi — field wajib hilang, tipe salah, batas=500 — ditolak
sebelum menyentuh handler. Aturan validasinya ditulis sekali, di tempat yang juga jadi dokumentasi.
Spec-first versus code-first
| Spec-first (oapi-codegen) | Code-first (anotasi swaggo) | |
|---|---|---|
| Sumber kebenaran | openapi.yaml | Komentar di kode |
| Dokumentasi bisa basi? | Tidak — compiler menjaganya | Ya — komentar bisa lupa diperbarui |
| Bisa dirancang sebelum kode | Ya — berguna untuk kesepakatan antar tim | Tidak |
| Kecepatan memulai | Lebih lambat | Lebih cepat |
| Klien untuk konsumen | Generate dari spesifikasi | Generate dari spesifikasi hasil ekspor |
| Cocok untuk | API publik, kontrak antar tim | API internal kecil |
Untuk API yang dikonsumsi tim lain, spec-first hampir selalu terbayar. Spesifikasi bisa di-review sebagai diff, disepakati sebelum ada satu baris kode, dan dipakai tim frontend untuk menghasilkan klien serta tiruan (mock) sejak hari pertama — sebelum backend-nya selesai.
Memberi versi pada API
| Perubahan | Aman? |
|---|---|
| Menambah field pada respons | ✅ selama klien mengabaikan yang tidak dikenal |
| Menambah field opsional pada permintaan | ✅ |
| Menambah endpoint | ✅ |
| Menghapus atau mengganti nama field | ❌ breaking |
| Memperketat validasi | ❌ breaking |
| Mengubah tipe field | ❌ breaking |
| Mengubah arti nilai yang sudah ada | ❌ breaking, dan yang paling sulit terdeteksi |
Latihan: tulis openapi.yaml untuk dua endpoint, generate ServerInterface,
dan pasang var _ api.ServerInterface = (*Server)(nil). Lalu tambahkan endpoint ketiga di
spesifikasi, generate ulang, dan lihat go build menolak sampai kamu menuliskannya. Itu
perbedaan antara dokumentasi dan kontrak.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.