"Dokumentasi API yang tidak sinkron dengan kode lebih berbahaya daripada tidak ada dokumentasi sama sekali — orang akan percaya sesuatu yang sudah tidak benar."
Tentang Artikel Ini
toko-api (project yang sama dari artikel-artikel sebelumnya) sekarang punya endpoint, validasi, dan autentikasi yang solid. Tapi kalau tim frontend bertanya "field apa saja yang wajib di POST /products?" — jawabanmu saat ini adalah "buka kodenya" atau "aku cek dulu". Itu yang artikel ini selesaikan.
Kita akan menulis OpenAPI 3.0 spec untuk toko-api — kontrak formal yang bisa dibaca manusia lewat Swagger UI, dan bisa dibaca mesin untuk validasi otomatis dan generate client code. Di akhir artikel, kamu akan punya pendekatan yang menghindari masalah paling umum dari dokumentasi API: spec yang berbohong karena tidak pernah diupdate.
Setelah selesai, kamu akan bisa:
- Menulis OpenAPI 3.0 spec untuk endpoint REST API
- Generate dokumentasi interaktif dengan Swagger UI
- Mendefinisikan schema yang reusable lewat
components - Validasi request/response otomatis terhadap spec saat runtime
- Menghasilkan OpenAPI spec langsung dari schema Zod yang sudah ada — bukan menulis dua kali
- Mencegah spec basi lewat pengecekan otomatis di CI
Prasyarat: Sudah menyelesaikan artikel #02 (REST API dengan Node.js & Express) — kita dokumentasikan toko-api yang sama, termasuk schema Zod dan middleware auth yang sudah ada di sana.
Daftar Isi
- Kenapa Dokumentasi API Penting: Kontrak, Bukan Catatan
- Anatomi OpenAPI 3.0
- Mendokumentasikan Endpoint: Paths & Operations
- Reusable Components: Schemas & Security
- Swagger UI: Dokumentasi Interaktif Otomatis
- Validasi Runtime: Request/Response Sesuai Spec
- Zod-First: Generate Spec dari Schema yang Sudah Ada
- Menjaga Spec Tetap Sinkron dengan Kode
Bab 1: Kenapa Dokumentasi API Penting: Kontrak, Bukan Catatan
Masalah yang Coba Diselesaikan
Tanpa dokumentasi formal, cara tim frontend tahu bentuk API-mu adalah: tanya di chat, baca kode, atau coba-coba lewat Postman sampai ketemu. Ini bekerja untuk tim kecil dengan komunikasi erat — tidak bekerja saat tim tumbuh, saat API dipakai tim eksternal, atau enam bulan lagi saat kamu sendiri lupa detail endpoint yang kamu tulis.
OpenAPI mengubah dokumentasi dari "catatan yang mungkin basi" menjadi kontrak — deskripsi formal, terstruktur, dan bisa divalidasi mesin tentang apa yang API-mu terima dan kembalikan.
Apa itu OpenAPI vs Swagger
Istilah ini sering tertukar. OpenAPI Specification (OAS) adalah standar format-nya (dulu bernama Swagger Specification, di-donate ke Linux Foundation tahun 2016). Swagger sekarang adalah nama brand untuk tooling di sekitar standar itu — Swagger UI, Swagger Editor, Swagger Codegen. Kamu menulis OpenAPI spec, lalu memakai Swagger UI untuk menampilkannya.
Manfaat Konkret, Bukan Sekadar "Rapi"
- Kontrak yang bisa dites — validasi otomatis bahwa API benar-benar sesuai yang didokumentasikan (Bab 6)
- Frontend bisa mulai kerja sebelum backend selesai — mock server dari spec, tanpa menunggu implementasi
- Generate client SDK otomatis — TypeScript types, HTTP client, dari satu sumber spec
- Onboarding developer baru lebih cepat — Swagger UI interaktif, bisa langsung coba endpoint dari browser
Bab 2: Anatomi OpenAPI 3.0
Struktur Dasar
# openapi.yaml
openapi: 3.0.3
info:
title: toko-api
description: REST API untuk sistem manajemen produk toko
version: 1.0.0
contact:
name: Tim Backend
email: dev@tokokamu.com
servers:
- url: https://api.tokokamu.com/api/v1
description: Production
- url: http://localhost:3000/api/v1
description: Local development
tags:
- name: Products
description: Operasi terkait produk
paths:
# Endpoint didefinisikan di sini (Bab 3)
components:
schemas:
# Schema reusable didefinisikan di sini (Bab 4)
securitySchemes:
# Skema autentikasi didefinisikan di sini (Bab 4)JSON atau YAML?
Keduanya valid — OpenAPI adalah format data, bukan format file. YAML lebih umum dipakai karena lebih mudah dibaca dan ditulis manusia (tidak ada kurung kurawal bersarang). Semua contoh di artikel ini pakai YAML.
Bab 3: Mendokumentasikan Endpoint: Paths & Operations
Anatomi Satu Operation
paths:
/products:
get:
tags: [Products]
summary: Ambil daftar produk
description: Mengembalikan daftar produk dengan pagination dan pencarian opsional.
parameters:
- name: page
in: query
schema:
type: integer
default: 1
description: Nomor halaman
- name: limit
in: query
schema:
type: integer
default: 10
maximum: 100
- name: search
in: query
schema:
type: string
description: Cari berdasarkan nama atau deskripsi produk
responses:
'200':
description: Daftar produk berhasil diambil
content:
application/json:
schema:
$ref: '#/components/schemas/ProductListResponse'
post:
tags: [Products]
summary: Buat produk baru
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProductInput'
responses:
'201':
description: Produk berhasil dibuat
content:
application/json:
schema:
$ref: '#/components/schemas/ProductResponse'
'400':
description: Validasi input gagal
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationErrorResponse'
'401':
description: Token tidak valid atau tidak adaPath Parameter
paths:
/products/{id}:
get:
tags: [Products]
summary: Ambil detail satu produk
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: ID produk
responses:
'200':
description: Produk ditemukan
content:
application/json:
schema:
$ref: '#/components/schemas/ProductResponse'
'404':
description: Produk tidak ditemukan
delete:
tags: [Products]
summary: Hapus produk (soft delete)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'204':
description: Produk berhasil dihapus
'403':
description: Hanya admin yang boleh menghapus produkPerhatikan responses mendokumentasikan semua status code yang mungkin dikembalikan — bukan cuma jalur sukses. Ini yang membuat spec berguna sebagai kontrak sungguhan, bukan sekadar contoh happy path.
Bab 4: Reusable Components: Schemas & Security
Kenapa components
Tanpa components, kamu akan menulis ulang struktur Product di setiap endpoint yang memakainya — GET /products, GET /products/:id, POST /products semuanya butuh bentuk yang sama atau mirip. components.schemas adalah tempat definisikan sekali, referensikan berkali-kali lewat $ref.
Schema Produk
components:
schemas:
Product:
type: object
properties:
id:
type: integer
example: 42
name:
type: string
example: "Kopi Arabika"
description:
type: string
nullable: true
price:
type: number
format: float
example: 45000
stock:
type: integer
example: 20
sku:
type: string
nullable: true
categoryId:
type: integer
nullable: true
createdAt:
type: string
format: date-time
CreateProductInput:
type: object
required: [name, price]
properties:
name:
type: string
minLength: 3
maxLength: 255
description:
type: string
price:
type: number
minimum: 0
stock:
type: integer
minimum: 0
default: 0
sku:
type: string
categoryId:
type: integer
ProductResponse:
type: object
properties:
success:
type: boolean
example: true
data:
$ref: '#/components/schemas/Product'
ProductListResponse:
type: object
properties:
success:
type: boolean
data:
type: array
items:
$ref: '#/components/schemas/Product'
meta:
type: object
properties:
page:
type: integer
totalPages:
type: integer
total:
type: integer
ValidationErrorResponse:
type: object
properties:
success:
type: boolean
example: false
message:
type: string
errors:
type: array
items:
type: object
properties:
field:
type: string
message:
type: stringBandingkan struktur CreateProductInput di atas dengan createProductSchema Zod dari artikel #02 — sengaja hampir identik. Di Bab 7 kita akan hilangkan duplikasi ini sepenuhnya.
Security Scheme untuk JWT
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: Token JWT dari endpoint /auth/login (artikel #04)security: [bearerAuth: []] di level operation (seperti di POST /products pada Bab 3) menandakan endpoint itu butuh header Authorization: Bearer <token> — persis middleware authenticate yang sudah kita bangun.
Bab 5: Swagger UI: Dokumentasi Interaktif Otomatis
Install dan Setup
npm install swagger-ui-express yaml// src/app.js (tambahan)
import swaggerUi from 'swagger-ui-express';
import { readFileSync } from 'fs';
import YAML from 'yaml';
const openapiDocument = YAML.parse(readFileSync('./openapi.yaml', 'utf-8'));
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiDocument));npm run dev
# Buka http://localhost:3000/api-docsSwagger UI otomatis merender spec jadi halaman dokumentasi interaktif — setiap endpoint bisa di-expand, lihat contoh request/response, dan langsung dicoba dari browser lewat tombol "Try it out" tanpa perlu Postman terpisah.
Serve Raw Spec untuk Tooling Lain
// Sediakan juga endpoint JSON mentah — dipakai oleh Postman import,
// code generator, atau tool CI di Bab 8
app.get('/openapi.json', (req, res) => {
res.json(openapiDocument);
});Bab 6: Validasi Runtime: Request/Response Sesuai Spec
Dari Dokumentasi ke Enforcement
Spec yang cuma didokumentasikan tapi tidak di-enforce bisa diam-diam jadi bohong — endpoint berubah, spec lupa diupdate, tidak ada yang tahu sampai frontend error. express-openapi-validator menutup celah ini: memvalidasi setiap request masuk terhadap spec, sebelum sempat menyentuh route handler.
npm install express-openapi-validator// src/app.js
import * as OpenApiValidator from 'express-openapi-validator';
app.use(
OpenApiValidator.middleware({
apiSpec: './openapi.yaml',
validateRequests: true,
validateResponses: process.env.NODE_ENV !== 'production', // matikan di production (ada overhead)
})
);
// Middleware ini harus dipasang SEBELUM routes
app.use('/api/v1', apiRouter);Dengan ini terpasang, request yang tidak sesuai spec (field wajib hilang, tipe data salah) ditolak otomatis dengan 400, sebelum kode controller-mu sempat jalan — bahkan sebelum middleware validate() dari Zod di artikel #02.
validateResponses: trueberguna saat development dan CI — dia akan melempar error kalau responsemu sendiri tidak sesuai spec yang kamu tulis. Ini menangkap kasus di mana implementasi berubah tapi spec lupa diupdate, dari sisi yang berlawanan (bukan cuma request klien yang divalidasi, tapi juga output API-mu sendiri).
Bab 7: Zod-First: Generate Spec dari Schema yang Sudah Ada
Masalah dengan Menulis Dua Kali
Bab 4 menunjukkan CreateProductInput di YAML yang isinya hampir identik dengan createProductSchema Zod di artikel #02. Ini bukan kebetulan — keduanya menjelaskan hal yang sama: bentuk data yang valid untuk membuat produk. Masalahnya, dua sumber kebenaran yang terpisah akan berbeda seiring waktu — seseorang menambah field baru di Zod schema, lupa update YAML, dan spec jadi bohong lagi persis masalah yang tadinya ingin kita selesaikan.
Solusinya: jadikan Zod schema sebagai satu-satunya sumber kebenaran, generate OpenAPI spec dari situ.
Setup zod-to-openapi
npm install @asteasolutions/zod-to-openapi// src/schemas/product.schema.js (perluasan dari artikel #02)
import { z } from 'zod';
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
extendZodWithOpenApi(z);
export const createProductSchema = z
.object({
name: z.string().min(3).max(255).openapi({ example: 'Kopi Arabika' }),
description: z.string().max(2000).optional(),
price: z.number().min(0).openapi({ example: 45000 }),
stock: z.number().int().min(0).default(0),
sku: z.string().optional(),
categoryId: z.number().int().positive().optional().nullable(),
})
.openapi('CreateProductInput'); // nama schema yang akan muncul di spec
export const productSchema = z
.object({
id: z.number(),
name: z.string(),
price: z.number(),
stock: z.number(),
createdAt: z.string(),
})
.openapi('Product');Ini schema yang sama persis yang sudah dipakai validate() middleware untuk validasi runtime di artikel #02 — hanya ditambah metadata .openapi() untuk keperluan dokumentasi. Tidak ada definisi kedua yang terpisah.
Generate Dokumen OpenAPI dari Registry
// scripts/generate-openapi.js
import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import { createProductSchema, productSchema } from '../src/schemas/product.schema.js';
import { writeFileSync } from 'fs';
import YAML from 'yaml';
const registry = new OpenAPIRegistry();
registry.register('CreateProductInput', createProductSchema);
registry.register('Product', productSchema);
registry.registerPath({
method: 'post',
path: '/products',
tags: ['Products'],
summary: 'Buat produk baru',
request: {
body: {
content: { 'application/json': { schema: createProductSchema } },
},
},
responses: {
201: {
description: 'Produk berhasil dibuat',
content: { 'application/json': { schema: productSchema } },
},
},
});
const generator = new OpenApiGeneratorV3(registry.definitions);
const document = generator.generateDocument({
openapi: '3.0.3',
info: { title: 'toko-api', version: '1.0.0' },
});
writeFileSync('./openapi.yaml', YAML.stringify(document));
console.log('✓ openapi.yaml ter-generate dari schema Zod');// package.json
{
"scripts": {
"docs:generate": "node scripts/generate-openapi.js"
}
}Trade-off: Kapan Ini Sepadan
Pendekatan ini menambah satu langkah build (npm run docs:generate) dan sedikit boilerplate registry.registerPath() per endpoint. Untuk API kecil dengan 3-4 endpoint, menulis YAML manual seperti Bab 3-4 mungkin memang lebih cepat. Tapi begitu API punya belasan endpoint dan lebih dari satu orang mengubahnya, dua sumber kebenaran yang harus disinkronkan manual adalah bom waktu — dan generate-dari-Zod menghilangkan kelas bug itu sepenuhnya.
Bab 8: Menjaga Spec Tetap Sinkron dengan Kode
Pengecekan Otomatis di CI
Kalau kamu pilih pendekatan generate-dari-Zod (Bab 7), tambahkan langkah CI yang memastikan openapi.yaml yang di-commit benar-benar hasil generate terbaru — bukan versi lama yang ketinggalan:
# .github/workflows/test.yml (tambahan dari artikel testing-api)
- name: Verifikasi openapi.yaml sinkron dengan schema
run: |
npm run docs:generate
git diff --exit-code openapi.yaml || \
(echo "❌ openapi.yaml belum di-generate ulang setelah perubahan schema. Jalankan 'npm run docs:generate' dan commit." && exit 1)Langkah ini gagal (dan memblokir merge) kalau ada perbedaan antara openapi.yaml yang di-commit dengan hasil generate ulang dari schema saat ini — memastikan spec tidak pernah diam-diam basi.
Kalau Menulis Spec Manual (Bab 3-4)
Tanpa generate otomatis, pertahanan realistisnya adalah validateResponses: true dari Bab 6 di lingkungan test/CI — kalau response API-mu menyimpang dari apa yang didokumentasikan di YAML, test suite akan gagal. Ini tidak sekuat pendekatan generate-dari-Zod (masih bisa lupa update YAML saat menambah endpoint baru), tapi jauh lebih baik daripada tidak ada pengecekan sama sekali.
Aturan minimal yang wajib dipegang, apapun pendekatannya: spec yang salah lebih buruk daripada tidak ada spec. Kalau tim tidak punya kapasitas menjaga spec tetap akurat, lebih baik tandai eksplisit "spec ini mungkin belum lengkap" daripada membiarkan orang percaya penuh pada sesuatu yang sudah menyimpang dari kode.
Penutup
toko-api sekarang punya dokumentasi yang bisa diklik, dicoba, dan divalidasi — bukan sekadar file README yang gampang basi.
Struktur Project Final
toko-api/
├── src/
│ ├── schemas/
│ │ └── product.schema.js # Zod schema + metadata .openapi()
│ └── ... (sudah ada dari artikel-artikel sebelumnya)
├── scripts/
│ └── generate-openapi.js # generate openapi.yaml dari Zod (opsional, Bab 7)
├── openapi.yaml # spec OpenAPI — sumber dokumentasi
└── package.json
Checklist Sebelum Bilang API-mu "Terdokumentasi dengan Baik"
- Semua endpoint punya
summarydandescriptionyang jelas - Semua status code yang mungkin dikembalikan terdokumentasi, bukan cuma jalur sukses
- Schema request/response pakai
components— tidak ada duplikasi struktur - Endpoint yang butuh auth punya
security: [bearerAuth: []] - Swagger UI bisa diakses dan "Try it out" benar-benar berfungsi
- Ada validasi otomatis (minimal
validateResponsesdi test/CI) yang mendeteksi spec yang menyimpang dari kode - Kalau pakai pendekatan Zod-first: CI menolak merge kalau
openapi.yamlbelum di-generate ulang
Langkah selanjutnya:
- Bandingkan paradigma API lain — sekarang kamu paham betul kontrak REST, lihat kapan GraphQL atau gRPC lebih cocok (artikel api-design-comparison)
- Otomatiskan lebih jauh — jalankan
docs:generatedan publish Swagger UI ke staging otomatis lewat CI/CD (artikel cicd-github-actions)