Lewati ke konten utama
Semua artikel
MenengahArsitektur

Dokumentasi API dengan OpenAPI & Swagger

Tulis API spec dengan OpenAPI 3.0, generate Swagger UI otomatis, validasi request/response, dan buat kontrak API yang bisa dipakai tim frontend tanpa ngobrol terus-terusan.

11 menit baca

"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

  1. Kenapa Dokumentasi API Penting: Kontrak, Bukan Catatan
  2. Anatomi OpenAPI 3.0
  3. Mendokumentasikan Endpoint: Paths & Operations
  4. Reusable Components: Schemas & Security
  5. Swagger UI: Dokumentasi Interaktif Otomatis
  6. Validasi Runtime: Request/Response Sesuai Spec
  7. Zod-First: Generate Spec dari Schema yang Sudah Ada
  8. 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"

  1. Kontrak yang bisa dites — validasi otomatis bahwa API benar-benar sesuai yang didokumentasikan (Bab 6)
  2. Frontend bisa mulai kerja sebelum backend selesai — mock server dari spec, tanpa menunggu implementasi
  3. Generate client SDK otomatis — TypeScript types, HTTP client, dari satu sumber spec
  4. 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 ada

Path 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 produk

Perhatikan 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: string

Bandingkan 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-docs

Swagger 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: true berguna 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 summary dan description yang 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 validateResponses di test/CI) yang mendeteksi spec yang menyimpang dari kode
  • Kalau pakai pendekatan Zod-first: CI menolak merge kalau openapi.yaml belum di-generate ulang

Langkah selanjutnya:

  1. Bandingkan paradigma API lain — sekarang kamu paham betul kontrak REST, lihat kapan GraphQL atau gRPC lebih cocok (artikel api-design-comparison)
  2. Otomatiskan lebih jauh — jalankan docs:generate dan publish Swagger UI ke staging otomatis lewat CI/CD (artikel cicd-github-actions)

Lanjutkan ke

REST API vs GraphQL vs gRPC — Kapan Pakai Apa?Segera
CI/CD untuk Backend Developer: GitHub ActionsSegera