Web Artisan
Beranda

Progress belajar

Modul 2 dari 3

0% 0/3 modul selesai

Setelah selesai, tandai modul ini agar progres kursus tetap rapi.

Progress disimpan lokal di browser ini.

Course · OpenAPI

Belajar OpenAPI
Kontrak HTTP yang Bisa Dieksekusi

OpenAPI bukan dokumentasi cantik, melainkan kontrak HTTP formal yang dimengerti manusia dan tooling. Kita rancang kontraknya dulu, lalu implementasi handler Go menyusul.

Spec: OpenAPI 3.1.1Default JSON Schema 2020-12~95 menit baca
01

Kenapa OpenAPI?

Dari dokumentasi yang cepat basi ke kontrak yang bisa diverifikasi

Bayangkan tim backend, frontend, mobile, QA, dan integrasi pihak ketiga semuanya menebak bentuk API dari sumber yang berbeda. Backend baca kode handler, frontend baca screenshot Postman, QA baca pesan Slack. Itulah kekacauan yang OpenAPI selesaikan.

OpenAPI Specification (OAS) adalah format standar yang language-agnostic untuk mendeskripsikan HTTP API. Dengan satu dokumen, manusia dan mesin bisa memahami endpoint apa saja yang tersedia, parameter dan body yang dibutuhkan, serta bentuk response yang dikembalikan, semuanya tanpa membaca source code atau menyadap traffic. Lihat OpenAPI Specification resmi untuk teks normatifnya.

Bedanya tajam: dokumentasi manusia ditulis untuk dibaca orang, sedangkan spec OpenAPI ditulis untuk dieksekusi tooling. Dari satu file openapi.yaml yang sama, tooling bisa membangkitkan dokumentasi interaktif, client TypeScript, mock server, dan test kontrak di CI. Dokumentasi yang ditulis tangan cepat basi karena ia terpisah dari kebenaran. Spec yang diverifikasi di CI tidak bisa berbohong tanpa ketahuan.

🌉Jembatan: dari TypeScript type dan Laravel API Resource

Di TypeScript kamu menulis interface Product agar frontend tahu bentuk data. Di Laravel kamu menulis API Resource agar response konsisten. OpenAPI menaikkan ide itu ke level kontrak HTTP penuh: bukan hanya bentuk data, tetapi juga path, method, status code, header, dan error, dalam satu file yang dipakai semua pihak sekaligus.

Inilah satu openapi.yaml paling sederhana untuk daftar produk skincare. Baca pelan-pelan, kita bedah strukturnya di section berikutnya.

api/openapi.yaml
openapi: 3.1.1 info: title: Skincare Shop API version: 1.0.0 paths: /v1/products: get: operationId: listProducts summary: Daftar produk skincare responses: "200": description: Daftar produk berhasil diambil content: application/json: schema: type: array items: type: object properties: id: type: integer name: type: string priceRupiah: type: integer

Walaupun pendek, file ini sudah berbicara banyak. Ia menyebut versi spec (3.1.1), identitas API (info), satu endpoint (GET /v1/products), nama unik operasi (operationId), dan bentuk response sukses (array produk dengan id, name, priceRupiah). Frontend bisa langsung tahu apa yang akan diterima, jauh sebelum handler Go pertama ditulis.

Single source of truth

Satu kontrak dipakai backend, frontend, mobile, QA, dan integrasi luar. Tidak ada lagi tebak-tebakan.

Machine-readable

Tooling membaca spec untuk codegen, mock, dan contract test. Dokumentasi manusia tak bisa dieksekusi.

Kontrak dulu, kode menyusul

Tim sepakat soal bentuk API sebelum satu handler pun ditulis, jadi paralelisasi kerja jadi mungkin.

📝Versi yang dipakai course ini

Versi spec OpenAPI terbaru adalah 3.2.0 (rilis 19 September 2025), sebuah feature release yang kompatibel mundur dengan 3.1. Course ini memakai 3.1.1 sebagai default karena dukungan tooling-nya paling luas, sambil tetap menjelaskan posisi 3.2 sebagai langkah berikutnya.

02

Mental Model dan Struktur Dokumen

Peta besar dari atas ke bawah, plus YAML vs JSON

Sebelum mengisi detail, kunci dulu peta besarnya. Dokumen OpenAPI punya beberapa field top-level yang selalu sama, dan memahami fungsinya membuat sisanya terasa mudah.

Sebuah dokumen OpenAPI 3.1 dibaca dari atas ke bawah dengan urutan logis: deklarasi versi, identitas, lokasi server, daftar endpoint, kumpulan komponen yang dipakai ulang, dan kebijakan keamanan global. Begitu kamu hafal kerangka ini, file sepanjang ribuan baris pun tetap bisa kamu navigasi.

flowchart TD
  Root["Dokumen OpenAPI 3.1.1"] --> V["openapi: versi spec"]
  Root --> Info["info: identitas API"]
  Root --> Servers["servers: base URL per environment"]
  Root --> Paths["paths: daftar endpoint"]
  Root --> Comp["components: schema, parameter, response reusable"]
  Root --> Sec["security: kebijakan auth global"]
  Paths --> Op["operation: get / post / put / patch / delete"]
  Op --> Params["parameters"]
  Op --> Body["requestBody"]
  Op --> Resp["responses"]

Gambar 1. Peta top-level dokumen OpenAPI. paths adalah jantungnya, components adalah lemari penyimpanan bersama.

FieldFungsiWajib?
openapiMenyatakan versi spec, misalnya 3.1.1. Menentukan aturan parsing.Ya
infoIdentitas API: title, version, description, kontak, lisensi.Ya
serversDaftar base URL tempat API berjalan (local, staging, production).Tidak
pathsKumpulan endpoint dan operasi HTTP-nya.Salah satu dari paths / components / webhooks
componentsObjek reusable: schemas, parameters, responses, securitySchemes.Tidak
securityKebijakan autentikasi default untuk seluruh operasi.Tidak
🌉Jembatan: dari folder routes dan controller

Di Express atau Laravel, struktur API tersebar di folder routes/ dan app/Http/Controllers/. OpenAPI menyatukan peta itu ke satu dokumen. paths adalah routes/api.php versi deklaratif, dan components.schemas adalah kumpulan API Resource atau TypeScript type yang dipakai ulang.

Spec yang sama bisa ditulis dalam YAML atau JSON. Keduanya setara, hanya beda format penulisan. YAML jauh lebih nyaman dibaca manusia karena ringkas dan mendukung komentar, sedangkan JSON lebih cocok saat dihasilkan atau dikonsumsi mesin. Course ini memakai YAML untuk contoh yang dibaca manusia.

YAML
  • Ringkas, indentasi-based, mendukung komentar dengan #.
  • Nyaman ditulis dan di-review tangan, ideal untuk file openapi.yaml.
  • Rawan salah indentasi karena struktur ditentukan spasi, bukan kurung.
JSON
  • Eksplisit dengan kurung kurawal dan kurung siku, tanpa komentar.
  • Cocok sebagai output tooling atau payload yang dikirim ke validator.
  • Lebih verbose untuk ditulis tangan, tetapi tidak ambigu soal struktur.

Potongan yang sama persis, ditulis dalam dua format. Perhatikan bahwa YAML memakai indentasi dua spasi sedangkan JSON memakai kurung eksplisit.

contoh.yaml (YAML)
info: title: Skincare Shop API version: 1.0.0
contoh.json (JSON)
{ "info": { "title": "Skincare Shop API", "version": "1.0.0" } }
⚠️Jebakan indentasi YAML

YAML memakai spasi, bukan tab, dan satu spasi nyasar bisa mengubah arti dokumen total. Sebuah field yang seharusnya anak dari info bisa diam-diam jadi field top-level kalau indentasinya meleset. Selalu pakai dua spasi per level dan aktifkan validator agar kesalahan struktur ketahuan lebih awal.

Inilah skeleton minimal openapi.yaml untuk Skincare Shop API yang akan kita isi sepanjang course. Semua field top-level ada, isinya masih kosong, siap diisi bertahap.

api/openapi.yaml (skeleton)
openapi: 3.1.1 info: title: Skincare Shop API version: 1.0.0 description: Kontrak HTTP untuk backend online shop skincare. servers: [] paths: {} components: schemas: {} parameters: {} responses: {} securitySchemes: {}
📝Kenapa 3.1.1 jadi default

OpenAPI 3.1.1 dipublikasikan 24 Oktober 2024 sebagai patch release dari 3.1.0. Ia hanya memperbaiki terminologi, penyelarasan JSON Schema, penanganan reference, dan kejelasan dokumentasi, tanpa perubahan breaking. Karena itu aman dipakai sebagai default dengan kompatibilitas tooling yang luas.

03

Info, Versi, dan Servers

Identitas kontrak dan tempat ia berjalan

Object info memberi API sebuah identitas, dan servers menjelaskan di mana API itu bisa dihubungi. Keduanya beda peran: info adalah tentang kontrak, servers adalah tentang deployment.

Mulai dari info. Ia berisi metadata yang membuat spec terasa seperti produk, bukan file mentah: judul, versi, deskripsi, kontak, dan lisensi. Field version di sini adalah versi kontrak API, bukan versi paket atau versi spec OpenAPI. Pakai semantic versioning agar perubahan kontrak punya makna yang jelas.

api/openapi.yaml (info)
info: title: Skincare Shop API version: 1.0.0 description: | Kontrak HTTP untuk backend online shop skincare. Mencakup katalog, cart, checkout, order, auth, dan admin. contact: name: Tim Backend Skincare email: backend@skincare.example license: name: Proprietary
🌉Jembatan: dari versioning package frontend

Di frontend kamu menaikkan versi paket di package.json saat ada perubahan, dan semver memberi tahu konsumen apakah aman upgrade. info.version melakukan hal yang sama untuk kontrak API. Naikkan patch untuk perbaikan tanpa dampak, minor untuk penambahan yang kompatibel, major untuk perubahan breaking.

Sekarang servers. Ia adalah daftar base URL tempat API berjalan. Satu kontrak yang sama biasanya berjalan di banyak environment: laptop developer, staging untuk QA, dan production untuk user nyata. Mencatat semuanya membuat client tahu ke mana harus menembak tanpa menebak.

api/openapi.yaml (servers)
servers: - url: http://localhost:8080 description: Local development - url: https://staging-api.skincare.example description: Staging untuk QA - url: https://api.skincare.example description: Production

Untuk URL yang sebagian dinamis, OpenAPI menyediakan server variables. Misalnya base URL yang berbeda hanya pada subdomain region. Variable diberi nilai default dan, bila perlu, daftar enum nilai yang sah.

api/openapi.yaml (server variables)
servers: - url: https://{region}.api.skincare.example description: Production per region variables: region: default: id enum: - id - sg description: Kode region deployment
🌉Jembatan: dari VITE_API_URL

Di proyek React kamu menyimpan VITE_API_URL di .env agar build tahu ke mana memanggil API. servers adalah versi terdokumentasi dan ter-review dari ide itu, hidup di dalam kontrak sehingga seluruh tim dan tooling melihat daftar environment yang sama.

💡Pisahkan kontrak dari environment

Kontrak API (path, schema, error) tidak berubah hanya karena pindah dari staging ke production. Yang berubah hanya base URL di servers. Menjaga pemisahan ini membuat satu spec bisa dipakai untuk semua environment tanpa duplikasi.

04

Paths, Operations, dan operationId

Jantung OpenAPI dan semantik HTTP method

paths adalah bagian terpenting dokumen. Di sinilah endpoint nyata dideklarasikan: kombinasi path, HTTP method, dan apa yang terjadi saat keduanya dipanggil.

Setiap entri di bawah paths adalah sebuah path item, misalnya /v1/products. Di dalamnya ada satu atau lebih operation object, satu per HTTP method (get, post, put, patch, delete). Tiap operation punya summary, description, tags, dan yang penting operationId: nama unik yang dipakai tooling dan codegen sebagai identitas operasi.

flowchart TD
  Paths["paths"] --> P1["/v1/products"]
  Paths --> P2["/v1/products/{productId}"]
  P1 --> G1["get: listProducts"]
  P1 --> C1["post: createProduct"]
  P2 --> G2["get: getProduct"]
  P2 --> U2["patch: updateProduct"]
  P2 --> D2["delete: deleteProduct"]

Gambar 2. Satu path item bisa punya banyak operation, satu per method. Tiap operation punya operationId unik.

api/openapi.yaml (paths)
paths: /v1/products: get: operationId: listProducts summary: Daftar produk skincare tags: [product] responses: "200": description: Daftar produk berhasil diambil /v1/products/{productId}: get: operationId: getProduct summary: Detail satu produk tags: [product] responses: "200": description: Produk ditemukan "404": description: Produk tidak ditemukan
🌉Jembatan: dari route name ke operationId

Di Laravel kamu memberi nama route dengan ->name('products.show') agar bisa dirujuk di seluruh aplikasi. Di Go kamu menamai fungsi handler GetProduct. operationId memainkan peran yang sama di OpenAPI: ia jadi nama fungsi yang dibangkitkan codegen, jadi pastikan unik dan deskriptif seperti getProduct, bukan get1.

Memilih method bukan soal selera, melainkan soal semantik HTTP. Method menyatakan niat operasi, dan client serta infrastruktur (cache, proxy, retry) bergantung pada makna itu. Dua sifat penting: safe (tidak mengubah state server) dan idempotent (memanggil berkali-kali memberi efek sama seperti sekali).

MethodNiatSafe?Idempotent?
GETMembaca resource tanpa efek samping.YaYa
POSTMembuat resource baru atau memicu aksi.TidakTidak
PUTMengganti representasi resource secara penuh.TidakYa
PATCHMengubah sebagian field resource.TidakTidak (umumnya)
DELETEMenghapus resource.TidakYa
🌉Jembatan: dari mutation React Query

Di React Query kamu memakai useQuery untuk baca (GET) dan useMutation untuk tulis (POST/PUT/PATCH/DELETE). Pembedaan itu bukan kebetulan; ia mencerminkan safe vs unsafe method. Mendesain spec dengan method yang tepat membuat caching dan retry di frontend bekerja sesuai harapan.

Inilah endpoint katalog skincare dengan method yang sesuai niatnya.

GET /v1/products Daftar produk dengan filter, search, dan pagination
GET /v1/products/{productId} Detail satu produk berdasarkan id
POST /v1/checkout Ubah keranjang jadi order dalam satu transaksi
DELETE /v1/orders/{orderId} Batalkan order yang belum dibayar
⚠️Jangan POST untuk segalanya

Godaan terbesar pemula adalah memakai POST untuk semua operasi, termasuk membaca data. Itu membuang manfaat caching GET, membingungkan client soal idempotency, dan membuat spec terbaca seperti RPC, bukan REST. Pakai method yang sesuai niat, bukan POST sebagai jalan pintas.

05

Parameter: Path, Query, dan Header

Tiga lokasi input yang berbeda peran

Input ke sebuah operasi bisa datang dari tiga lokasi: path, query string, dan header. Tiap lokasi punya peran khasnya sendiri, dan OpenAPI mendeskripsikan ketiganya lewat parameter object dengan field in.

flowchart LR
  Req["GET /v1/products/{productId}?sort=price"] --> Path["in: path -> productId (identitas resource)"]
  Req --> Query["in: query -> sort, page, q (filter dan opsi)"]
  Req --> Header["in: header -> Authorization, X-Request-ID (metadata)"]

Gambar 3. Tiga lokasi parameter dan peran masing-masing dalam satu request.

Mulai dari path parameter. Ia merepresentasikan identitas resource dan selalu bagian dari URL, misalnya productId di /v1/products/{productId}. Path parameter selalu required, karena tanpa nilainya path tidak lengkap.

api/openapi.yaml (path parameter)
/v1/products/{productId}: get: operationId: getProduct parameters: - name: productId in: path required: true description: Identitas unik produk schema: type: integer format: int64 example: 42
🌉Jembatan: dari React Router :id dan Laravel {product}

Di React Router kamu menulis /products/:id, di Laravel /products/{product}. OpenAPI memakai sintaks {productId} yang sama, tetapi menambah deklarasi tipe dan contoh nilai. Jadi bukan hanya “ada parameter di sini”, melainkan “parameter ini integer 64-bit dengan contoh 42”.

Query parameter cocok untuk hal yang bukan identitas: filter, search, sort, dan pagination. Untuk katalog skincare, query yang lazim adalah page, limit, sort, q (kata kunci pencarian), dan skin_type. Query parameter umumnya opsional dengan nilai default yang masuk akal.

api/openapi.yaml (query parameter)
/v1/products: get: operationId: listProducts parameters: - name: q in: query required: false description: Kata kunci pencarian nama produk schema: type: string example: serum vitamin c - name: skin_type in: query required: false schema: type: string enum: [oily, dry, combination, sensitive, normal] example: oily - name: page in: query schema: type: integer minimum: 1 default: 1 - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20
🌉Jembatan: dari URLSearchParams

Di frontend kamu menyusun query dengan new URLSearchParams({ page: '1', skin_type: 'oily' }). OpenAPI mendeskripsikan sisi kontrak dari string itu: nama parameter, tipe, nilai enum yang sah, dan default. Frontend jadi tahu skin_type=banana akan ditolak sebelum mengirimnya.

Request header menyampaikan metadata, bukan data bisnis: kredensial auth, idempotency key, dan trace id. Kunci penting di sini adalah selektif. Dokumentasikan header yang menjadi kontrak (yang harus dikirim atau diperhatikan client), bukan setiap header internal.

api/openapi.yaml (header parameter)
/v1/checkout: post: operationId: checkout parameters: - name: Idempotency-Key in: header required: true description: UUID unik agar checkout tidak terjadi dua kali saat retry schema: type: string format: uuid example: 9f1c7e2a-1d4b-4a9e-8a1f-3c2b5d6e7f80 - name: X-Request-ID in: header required: false description: ID korelasi untuk tracing antar service schema: type: string
🌉Jembatan: dari Axios interceptor

Di frontend kamu sering memasang Authorization lewat Axios interceptor untuk semua request. Di OpenAPI, Authorization biasanya tidak ditulis sebagai parameter header biasa, melainkan lewat securitySchemes (lihat section Security Schemes). Header lain seperti Idempotency-Key tetap didokumentasikan sebagai parameter in: header.

💡required, type, enum, example

Untuk tiap parameter, isi empat hal yang membuat kontrak hidup: required (wajib atau tidak), type/format (bentuk data), enum (nilai sah bila terbatas), dan example (contoh nyata). Example yang baik membuat dokumentasi terasa konkret dan menjadi bahan untuk mock server.

06

Request Body

Mendeskripsikan payload yang dikirim client

Saat client membuat resource, memperbarui data, login, atau checkout, ia mengirim payload. Object requestBody mendeskripsikan payload itu: apakah wajib, dalam media type apa, dan menautkannya ke schema.

Berbeda dari parameter yang tersebar di path, query, dan header, request body adalah satu kesatuan yang dikirim di body request. Field utamanya: required (apakah body wajib ada), content (peta media type ke schema), dan di dalamnya media type seperti application/json.

flowchart LR
  RB["requestBody"] --> Req["required: true/false"]
  RB --> Content["content"]
  Content --> MT["application/json"]
  MT --> Schema["schema (bentuk payload)"]

Gambar 4. Anatomi requestBody: dari flag required, ke media type, ke schema payload.

Contoh paling jelas adalah login. Client mengirim email dan password, dan body itu wajib ada. Schema-nya inline dulu untuk kejelasan; di section Components kita pindahkan ke tempat reusable.

api/openapi.yaml (request body login)
/v1/auth/login: post: operationId: login summary: Login dengan email dan password tags: [auth] requestBody: required: true content: application/json: schema: type: object required: [email, password] properties: email: type: string format: email example: dina@skincare.example password: type: string format: password minLength: 8 example: rahasiakuat123 responses: "200": description: Login berhasil
🌉Jembatan: dari form state dan Laravel Form Request

Di React kamu menyimpan input form di state lalu mengirimnya sebagai JSON. Di Laravel, Form Request mendefinisikan aturan validasi payload masuk. requestBody dengan schema-nya adalah kontrak deklaratif yang menyatukan keduanya: bentuk payload yang frontend kirim sekaligus aturan yang backend validasi.

Checkout lebih kaya. Body-nya berisi daftar item, alamat pengiriman, dan metode pembayaran. Perhatikan bagaimana schema bersarang menggambarkan struktur nyata payload.

api/openapi.yaml (request body checkout)
/v1/checkout: post: operationId: checkout summary: Ubah keranjang jadi order tags: [checkout] requestBody: required: true content: application/json: schema: type: object required: [items, shippingAddress, paymentMethod] properties: items: type: array minItems: 1 items: type: object required: [productId, quantity] properties: productId: type: integer format: int64 quantity: type: integer minimum: 1 shippingAddress: type: string example: Jl. Melati No. 7, Bandung paymentMethod: type: string enum: [bank_transfer, ewallet, cod] responses: "201": description: Order berhasil dibuat
💡Body hanya untuk method yang menulis

GET tidak boleh punya request body yang bermakna; ia membaca, bukan mengirim payload. Pakai requestBody di POST, PUT, dan PATCH. Untuk DELETE, body jarang diperlukan dan banyak tooling mengabaikannya, jadi sandarkan identitas resource ke path parameter.

07

Response Object: Bukan Hanya Happy Path

Mendeskripsikan semua kemungkinan hasil operasi

Kontrak yang baik tidak hanya bilang “kalau sukses, ini bentuknya”. Ia juga memprediksi kegagalan: input salah, tidak terautentikasi, resource tidak ada, konflik, validasi gagal, dan error server. Object responses mendeskripsikan semua itu.

Tiap entri di responses dikunci oleh status code HTTP, dan tiap status punya description, opsional content (body response), dan opsional headers. Menutup semua status yang realistis membuat backend dan frontend punya ekspektasi yang sama sejak awal, bukan saling kaget di production.

stateDiagram-v2
  [*] --> Validasi
  Validasi --> Auth: input valid
  Validasi --> E400: input salah (400)
  Validasi --> E422: validasi gagal (422)
  Auth --> Proses: terautentikasi
  Auth --> E401: tanpa token (401)
  Proses --> Sukses: resource ada
  Proses --> E404: tidak ditemukan (404)
  Proses --> E409: konflik stok (409)
  Proses --> E500: error server (500)
  Sukses --> [*]

Gambar 5. Satu operasi checkout bisa berakhir di banyak status. Kontrak yang baik menulis semuanya.

StatusArtiKapan di skincare shop
200OK, request berhasil.GET produk, GET order.
201Created, resource baru dibuat.POST checkout membuat order.
400Bad Request, request tidak terbentuk benar.JSON rusak, parameter salah tipe.
401Unauthorized, butuh autentikasi.Akses cart tanpa token.
404Not Found, resource tidak ada.GET produk dengan id yang tidak ada.
409Conflict, bentrok dengan state saat ini.Checkout saat stok habis.
422Unprocessable Entity, validasi semantik gagal.Email tidak valid, quantity nol.
500Internal Server Error, kesalahan server.Database down saat memproses order.
api/openapi.yaml (responses lengkap)
/v1/checkout: post: operationId: checkout responses: "201": description: Order berhasil dibuat headers: Location: description: URL order yang baru dibuat schema: type: string content: application/json: schema: type: object properties: orderId: type: integer format: int64 status: type: string example: pending_payment "401": description: Tidak terautentikasi "409": description: Stok tidak mencukupi "422": description: Validasi payload gagal "500": description: Kesalahan server
🌉Jembatan: dari try/catch ke status yang diprediksi

Di frontend kamu menulis try/catch lalu menebak error apa yang mungkin muncul. Dengan responses yang lengkap, kamu tidak menebak lagi: spec sudah memberi tahu bahwa checkout bisa mengembalikan 401, 409, 422, atau 500, masing-masing dengan makna jelas, sehingga UI bisa menyiapkan pesan untuk tiap kasus.

⚠️Happy path saja itu jebakan

Spec yang hanya menulis 200 adalah kontrak setengah jadi. Frontend akan menulis penanganan error berdasarkan tebakan, lalu pecah saat backend mengembalikan 409 yang tak terduga. Tuliskan status error yang realistis; itu bagian dari kontrak, bukan tambahan opsional.

08

Schema Object dan JSON Schema

Mendefinisikan bentuk data dengan presisi

Sampai di sini kita banyak menulis schema inline tanpa membahas isinya. Saatnya mendalami Schema Object: bagian yang mendefinisikan bentuk data, dari tipe sampai constraint detail seperti panjang minimum dan nilai minimum.

Schema Object menjawab pertanyaan “data ini bentuknya seperti apa”: type, properties, required, nullable, enum, format, items (untuk array), serta constraint seperti minLength, maxLength, minimum, dan maximum. Inilah yang membuat spec bisa memvalidasi payload, bukan sekadar mendeskripsikannya.

📝OpenAPI 3.1 dan JSON Schema 2020-12

OpenAPI 3.1 selaras penuh dengan JSON Schema Draft 2020-12. Dialect default-nya adalah https://spec.openapis.org/oas/3.1/dialect/base, yaitu JSON Schema 2020-12 plus vocabulary khas OpenAPI. Artinya pengetahuan JSON Schema yang sudah kamu punya langsung berlaku, dan schema-mu konsisten dengan ekosistem JSON modern.

🌉Jembatan: dari TypeScript interface

Schema OpenAPI adalah saudara deklaratif dari TypeScript interface. Bedanya, schema bisa membawa constraint runtime yang TypeScript tidak bisa: minLength: 8, minimum: 1, format: email. Jadi schema bukan hanya bentuk, tetapi juga aturan validasi yang bisa ditegakkan tooling.

Mari modelkan domain skincare. Pertama Product, dengan field id, name, dan referensi ke variant dan harga. Perhatikan constraint pada tiap field.

api/openapi.yaml (schema Product)
Product: type: object required: [id, name, slug, priceRupiah, isActive] properties: id: type: integer format: int64 example: 42 name: type: string minLength: 1 maxLength: 200 example: Serum Vitamin C 20% slug: type: string example: serum-vitamin-c-20 description: type: string nullable: true priceRupiah: type: integer format: int64 minimum: 0 description: Harga dalam rupiah sebagai bilangan bulat example: 149000 skinTypes: type: array items: type: string enum: [oily, dry, combination, sensitive, normal] isActive: type: boolean example: true
⚠️Uang bukan float

Harga ditulis sebagai integer rupiah (priceRupiah), bukan number desimal. Float menyimpan uang membawa galat pembulatan yang berbahaya untuk perhitungan total dan pajak. Simpan satuan terkecil sebagai bilangan bulat. Di Go, ini sejalan dengan menyimpan PriceRupiah int64.

ProductVariant mewakili varian (misalnya ukuran 30ml dan 50ml) yang masing-masing punya harga dan stok sendiri.

api/openapi.yaml (schema ProductVariant)
ProductVariant: type: object required: [id, label, priceRupiah, stock] properties: id: type: integer format: int64 label: type: string example: 30ml priceRupiah: type: integer format: int64 minimum: 0 stock: type: integer minimum: 0 example: 24

Untuk uang yang punya satuan dan mata uang eksplisit, sebuah schema Money kecil membuat representasi konsisten lintas API. Ini juga jadi contoh perubahan kontrak yang kita bahas di section Topik Lanjutan.

api/openapi.yaml (schema Money)
Money: type: object required: [amount, currency] properties: amount: type: integer format: int64 minimum: 0 description: Jumlah dalam satuan terkecil (rupiah penuh) example: 149000 currency: type: string enum: [IDR] example: IDR

Terakhir, dasar ErrorResponse yang akan kita matangkan di section Error Standard.

api/openapi.yaml (schema ErrorResponse dasar)
ErrorResponse: type: object required: [code, message] properties: code: type: string example: PRODUCT_NOT_FOUND message: type: string example: Produk tidak ditemukan requestId: type: string example: 9f1c7e2a-1d4b-4a9e-8a1f-3c2b5d6e7f80
💡required itu daftar nama, bukan flag per field

Berbeda dari sebagian bahasa, required di JSON Schema adalah array nama field di level object, bukan atribut pada tiap field. Sebuah field dianggap wajib kalau namanya tercantum di array required object induknya. nullable: true adalah hal terpisah: ia memperbolehkan nilai null, bukan menghilangkan kewajiban.

09

Components, $ref, Examples, dan Tags

Empat alat menjaga spec tetap maintainable

Begitu spec tumbuh, duplikasi jadi musuh. Schema Product yang ditulis ulang di lima tempat akan menyimpang diam-diam. Empat alat ini menjaga spec tetap ramping dan mudah dinavigasi: components, $ref, examples, dan tags.

components adalah lemari penyimpanan objek reusable: schemas, parameters, responses, dan securitySchemes. Setelah objek diletakkan di sana, ia dirujuk dari mana saja dengan $ref, sebuah pointer ke lokasi definisi. Tidak ada lagi salin-tempel.

api/openapi.yaml (components dan $ref)
paths: /v1/products/{productId}: get: operationId: getProduct responses: "200": description: Produk ditemukan content: application/json: schema: $ref: "#/components/schemas/Product" "404": $ref: "#/components/responses/NotFound" components: schemas: Product: type: object required: [id, name] properties: id: { type: integer, format: int64 } name: { type: string } responses: NotFound: description: Resource tidak ditemukan content: application/json: schema: $ref: "#/components/schemas/ErrorResponse"
🌉Jembatan: dari shared types dan Laravel Resource

Di TypeScript kamu mengekspor interface Product dari satu file lalu mengimpornya di banyak tempat. Di Laravel, satu API Resource class dipakai ulang di banyak controller. components plus $ref adalah versi OpenAPI dari pola itu: definisikan sekali, rujuk berkali-kali, ubah di satu tempat.

examples membuat spec terasa nyata. Sebuah schema memberi tahu bentuk, tetapi example memberi tahu rupanya. Kamu bisa menaruh satu example di schema, atau beberapa examples bernama di media type untuk menunjukkan kasus berbeda. Example juga jadi bahan dasar mock server.

api/openapi.yaml (examples bernama)
content: application/json: schema: $ref: "#/components/schemas/Product" examples: serum: summary: Serum vitamin C value: id: 42 name: Serum Vitamin C 20% priceRupiah: 149000 sunscreen: summary: Sunscreen SPF 50 value: id: 77 name: Daily Sunscreen SPF 50 PA++++ priceRupiah: 99000
🌉Jembatan: dari Storybook mock data

Di Storybook kamu menyiapkan mock data agar komponen bisa dirender tanpa backend. examples di OpenAPI memberi peran yang sama untuk seluruh API: data contoh yang nyata, sehingga dokumentasi interaktif dan mock server bisa langsung memamerkan response yang masuk akal.

tags mengelompokkan operation per domain agar dokumentasi tidak jadi daftar endpoint mentah yang panjang. Untuk skincare shop, kelompokkan per domain: product, cart, checkout, order, auth, dan admin.

api/openapi.yaml (tags)
tags: - name: product description: Katalog produk dan pencarian - name: cart description: Keranjang belanja - name: checkout description: Proses checkout jadi order - name: order description: Riwayat dan status order - name: auth description: Login, register, dan sesi - name: admin description: Operasi khusus admin
💡Satu operasi, satu tag domain

Beri tiap operation tag domain yang jelas lewat tags: [product]. Tooling dokumentasi memakai tag untuk menyusun sidebar, jadi pengelompokan yang rapi langsung terlihat oleh siapa pun yang membuka spec. Hindari tag generik seperti misc yang akhirnya menampung segalanya.

10

Error Response Standard

Satu bentuk error yang konsisten lintas endpoint

Bila tiap endpoint mengembalikan error dengan bentuk berbeda, frontend, mobile, dan QA terpaksa menebak. Error standard yang konsisten membuat penanganan error bisa ditulis sekali dan dipakai di mana saja.

Bentuk error yang baik membawa minimal: kode mesin yang stabil (code), pesan untuk manusia (message), detail validasi per field bila ada (fields), dan id request untuk korelasi log (requestId). Kode mesin penting karena pesan bisa berubah kata-katanya, tetapi code jadi pegangan logika di client.

api/openapi.yaml (ErrorResponse dan ValidationErrorResponse)
components: schemas: ErrorResponse: type: object required: [code, message] properties: code: type: string example: PRODUCT_NOT_FOUND message: type: string example: Produk tidak ditemukan requestId: type: string format: uuid ValidationErrorResponse: type: object required: [code, message, fields] properties: code: type: string example: VALIDATION_FAILED message: type: string example: Beberapa field tidak valid fields: type: array items: type: object required: [field, message] properties: field: type: string example: email message: type: string example: Format email tidak valid requestId: type: string format: uuid
🌉Jembatan: dari Laravel validation error bag

Laravel mengembalikan error validasi sebagai object errors dengan key per field dan array pesan. ValidationErrorResponse di atas menstandarkan ide itu ke seluruh API, sehingga frontend bisa menulis satu komponen form error yang berlaku untuk semua endpoint, bukan satu per controller.

Untuk standar industri yang matang, ada RFC 9457 (Problem Details for HTTP APIs). Ia mendefinisikan media type application/problem+json dengan field standar type, title, status, detail, dan instance, plus extension seperti errors. RFC ini menggantikan RFC 7807 dan layak jadi acuan saat kamu ingin bentuk error yang interoperable lintas organisasi. Lihat RFC 9457 untuk teks lengkapnya.

api/openapi.yaml (Problem Details ala RFC 9457)
components: schemas: Problem: type: object properties: type: type: string format: uri example: https://skincare.example/problems/out-of-stock title: type: string example: Stok tidak mencukupi status: type: integer example: 409 detail: type: string example: Varian 30ml dari produk 42 sudah habis instance: type: string example: /v1/checkout
Contoh response Problem Details
HTTP/1.1 409 Conflict Content-Type: application/problem+json { "type": "https://skincare.example/problems/out-of-stock", "title": "Stok tidak mencukupi", "status": 409, "detail": "Varian 30ml dari produk 42 sudah habis", "instance": "/v1/checkout" }
💡Pilih satu, lalu konsisten

Kamu boleh memakai ErrorResponse buatan sendiri atau Problem Details RFC 9457. Yang penting bukan mana yang dipilih, melainkan konsistensinya: satu bentuk error untuk seluruh API. Bentuk yang berganti-ganti antar endpoint menghapus semua manfaat standardisasi.

11

Security Schemes

Mendokumentasikan autentikasi dengan jelas

API nyata punya endpoint publik (daftar produk) dan terproteksi (checkout, order). components.securitySchemes mendokumentasikan cara autentikasi, dan field security menerapkannya secara global atau per operasi.

OpenAPI mendukung beberapa jenis skema keamanan. Yang paling umum di backend Go: bearer token (JWT) lewat header Authorization, API key lewat header khusus, dan OAuth2 untuk alur yang lebih kompleks. Definisikan skemanya sekali di components.securitySchemes, lalu rujuk dengan namanya.

api/openapi.yaml (securitySchemes)
components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: JWT pada header Authorization, format "Bearer <token>" apiKeyAuth: type: apiKey in: header name: X-API-Key description: API key untuk integrasi server ke server
🌉Jembatan: dari middleware auth

Di Go kamu memasang middleware yang memeriksa header Authorization sebelum handler jalan; di Laravel kamu memakai middleware auth:sanctum. securitySchemes adalah deklarasi kontrak dari middleware itu: ia memberi tahu client cara mengirim kredensial, jauh sebelum melihat kode middleware-nya.

Penerapan keamanan bertingkat. security di level top-level berlaku untuk semua operasi sebagai default. Operasi tertentu bisa override, termasuk membuat dirinya publik dengan security: [] (array kosong berarti tidak butuh auth).

api/openapi.yaml (security global dan override)
security: - bearerAuth: [] paths: /v1/products: get: operationId: listProducts summary: Daftar produk (publik) security: [] responses: "200": description: Daftar produk /v1/checkout: post: operationId: checkout summary: Checkout (butuh login) responses: "201": description: Order dibuat "401": description: Tidak terautentikasi
flowchart TD
  Global["security global: bearerAuth"] --> List["GET /v1/products -> override security: [] (publik)"]
  Global --> Checkout["POST /v1/checkout -> pakai default (butuh JWT)"]
  Global --> Orders["GET /v1/orders -> pakai default (butuh JWT)"]

Gambar 6. Security global jadi default; daftar produk override menjadi publik, sedangkan checkout dan order tetap terproteksi.

⚠️Endpoint publik harus override eksplisit

Jika security global menuntut JWT dan kamu lupa menambahkan security: [] pada daftar produk, spec menyatakan endpoint itu butuh auth, padahal niatnya publik. Selalu tandai endpoint publik secara eksplisit agar kontrak cocok dengan implementasi.

12

Blueprint: Spec Lengkap Online Shop Skincare

Menyatukan semua konsep jadi satu kontrak utuh

Sekarang seluruh konsep berkumpul. Inilah blueprint openapi.yaml untuk MVP online shop skincare: katalog, cart, checkout, order, auth, dan admin, dengan tags domain, shared schemas, error standard, security, dan examples.

Sebelum melihat YAML utuh, ini struktur folder tempat spec hidup di proyek Go. Spec berdiri sendiri di api/, terpisah dari kode implementasi.

Posisi openapi.yaml di proyek Go
  • skincare-backend/
  • api/
  • openapi.yaml kontrak API, single source of truth
  • cmd/
  • api/
  • main.go entry point server
  • internal/
  • http/ handler dan router (implementasi kontrak)
  • domain/ tipe domain: Product, Order, Money
  • go.mod module github.com/kamu/skincare-backend

Inilah daftar endpoint MVP, dikelompokkan per domain. Publik vs terproteksi ditandai oleh ada tidaknya kebutuhan auth.

GET /v1/products Publik. Daftar produk dengan filter dan pagination
GET /v1/products/{productId} Publik. Detail satu produk
POST /v1/auth/login Publik. Login dan terima JWT
GET /v1/cart Terproteksi. Isi keranjang user
POST /v1/checkout Terproteksi. Ubah keranjang jadi order
GET /v1/orders/{orderId} Terproteksi. Status satu order
POST /v1/admin/products Admin. Buat produk baru

Dan inilah kerangka spec yang menyatukan semuanya. Sengaja dipangkas pada bagian berulang, tetapi strukturnya utuh dan valid sebagai blueprint.

api/openapi.yaml (blueprint MVP)
openapi: 3.1.1 info: title: Skincare Shop API version: 1.0.0 description: Kontrak HTTP untuk backend online shop skincare (MVP). servers: - url: http://localhost:8080 description: Local development - url: https://api.skincare.example description: Production security: - bearerAuth: [] tags: - name: product - name: cart - name: checkout - name: order - name: auth - name: admin paths: /v1/products: get: operationId: listProducts summary: Daftar produk tags: [product] security: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/Limit" - name: skin_type in: query schema: type: string enum: [oily, dry, combination, sensitive, normal] responses: "200": description: Daftar produk content: application/json: schema: type: array items: $ref: "#/components/schemas/Product" /v1/products/{productId}: get: operationId: getProduct summary: Detail produk tags: [product] security: [] parameters: - name: productId in: path required: true schema: type: integer format: int64 responses: "200": description: Produk ditemukan content: application/json: schema: $ref: "#/components/schemas/Product" "404": $ref: "#/components/responses/NotFound" /v1/auth/login: post: operationId: login summary: Login tags: [auth] security: [] requestBody: required: true content: application/json: schema: type: object required: [email, password] properties: email: { type: string, format: email } password: { type: string, minLength: 8 } responses: "200": description: Login berhasil "422": $ref: "#/components/responses/ValidationError" /v1/checkout: post: operationId: checkout summary: Checkout tags: [checkout] parameters: - name: Idempotency-Key in: header required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CheckoutRequest" responses: "201": description: Order dibuat "401": $ref: "#/components/responses/Unauthorized" "409": $ref: "#/components/responses/Conflict" components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT parameters: Page: name: page in: query schema: { type: integer, minimum: 1, default: 1 } Limit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 20 } responses: NotFound: description: Resource tidak ditemukan content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" Unauthorized: description: Tidak terautentikasi content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" Conflict: description: Konflik dengan state saat ini content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" ValidationError: description: Validasi gagal content: application/json: schema: $ref: "#/components/schemas/ValidationErrorResponse" schemas: Product: type: object required: [id, name, priceRupiah, isActive] properties: id: { type: integer, format: int64 } name: { type: string, minLength: 1, maxLength: 200 } priceRupiah: { type: integer, format: int64, minimum: 0 } isActive: { type: boolean } CheckoutRequest: type: object required: [items, paymentMethod] properties: items: type: array minItems: 1 items: type: object required: [productId, quantity] properties: productId: { type: integer, format: int64 } quantity: { type: integer, minimum: 1 } paymentMethod: type: string enum: [bank_transfer, ewallet, cod] ErrorResponse: type: object required: [code, message] properties: code: { type: string } message: { type: string } requestId: { type: string, format: uuid } ValidationErrorResponse: type: object required: [code, message, fields] properties: code: { type: string } message: { type: string } fields: type: array items: type: object required: [field, message] properties: field: { type: string } message: { type: string }
💡Blueprint dulu, handler kemudian

File ini adalah blueprint yang disepakati tim sebelum satu handler Go ditulis. Frontend bisa mulai dengan mock dari spec ini, QA bisa menyiapkan test, dan backend punya peta jelas endpoint apa yang harus dibangun. Kontrak dulu, implementasi menyusul.

13

Topik Lanjutan dan Langkah Berikutnya

Yang sengaja ditunda agar tidak hilang diam-diam

Course ini fokus pada menulis kontrak. Ada banyak topik kuat di sekitar OpenAPI yang sengaja ditunda agar fondasi kontrak ini kuat dulu. Berikut peta singkatnya supaya tidak ada yang hilang.

Contract-first vs code-first

Menulis spec sebelum kode (design-first) atau menghasilkan spec dari kode. Tradeoff utamanya adalah drift dan alur review.

OpenAPI dengan Go

Posisi spec dalam arsitektur Go: api/openapi.yaml, internal/http, internal/domain, plus generated stub dan validation middleware.

Generate client TypeScript

Membangkitkan client dan type TypeScript dari spec agar frontend tidak menebak kontrak.

Mock server

Menjalankan mock API dari examples agar frontend dan backend bisa kerja paralel.

Contract testing

Memvalidasi request dan response implementasi terhadap spec di CI, agar docs dan kode tidak menyimpang diam-diam.

Linting dan style guide

Naming convention, required description, operationId uniqueness, tag policy, dan error response policy yang dijaga di CI.

Breaking change dan API review

Membedakan perubahan breaking (hapus field, rename, ubah type) dari non-breaking (tambah optional), lalu mereviewnya dengan disiplin.

API versioning

URL versioning, gambaran header versioning, dan kapan perubahan dianggap breaking, misalnya /v1/products versus rencana /v2/products.

Webhooks

Field top-level webhooks (didukung sejak OpenAPI 3.1) untuk callback payment gateway, misalnya event PaymentSucceeded.

Cookie session dan CSRF

Cookie auth scheme, header CSRF, dan same-site behavior untuk API berbasis SPA, mirip pola Laravel Sanctum.

Dua catatan teknis untuk Go yang sering ditanyakan lebih awal: ada dua pendekatan utama menghubungkan OpenAPI dengan kode Go, masing-masing punya filosofi berbeda.

oapi-codegen (spec-first)
  • Spec adalah sumber kebenaran; kode Go dibangkitkan darinya.
  • Membangkitkan client dan server stub Go dari spec OpenAPI 3.
  • Install: go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest.
swaggo/swag (code-first)
  • Kode adalah sumber kebenaran; spec dibangkitkan dari anotasi handler.
  • Anotasi pada handler diubah jadi spec lewat swag init.
  • Cocok bila tim lebih nyaman mulai dari kode, dengan risiko drift bila anotasi tertinggal.
Terminal (instalasi oapi-codegen)
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
📝Webhooks sejak 3.1, dan ke depan 3.2

Field top-level webhooks adalah fitur baru di OpenAPI 3.1.0 untuk mendeskripsikan webhook out-of-band; patch 3.1.1 tidak mengubahnya. Versi 3.2.0 (rilis September 2025) menambah fitur lagi sambil tetap kompatibel mundur dengan 3.1, jadi spec 3.1 tetap berjalan.

💡Kelompok tooling ini layak jadi course tersendiri

Codegen, mock server, contract testing, linting, dan integrasi CI adalah ekosistem yang luas. Semuanya layak menjadi course terpisah OpenAPI Lanjutan di masa depan. Course ini memberimu fondasi kontrak yang kokoh, yang menjadi prasyarat semua itu.

14

Ringkasan dan Poin Penting

Benang merah dan jalan ke implementasi

Satu benang merah mengikat seluruh course: paths, operations, parameter, request body, responses, schema, components, security, dan examples bukan bagian-bagian terpisah, melainkan satu kontrak yang bisa diverifikasi.

Kita mulai dari motivasi (OpenAPI sebagai kontrak machine-readable, bukan dokumentasi cantik), membangun mental model struktur dokumen, lalu mengisi tiap bagian: identitas dan server, endpoint dan method, tiga lokasi parameter, request body, response yang menutup error, schema yang presisi, components untuk reuse, error standard, dan security. Semuanya berpuncak pada satu blueprint openapi.yaml untuk online shop skincare.

flowchart LR
  Spec["openapi.yaml (kontrak)"] --> Impl["Implementasi Go: net/http + chi"]
  Impl --> Validation["Handler validation"]
  Validation --> CI["CI contract check"]
  Spec --> CI
  CI --> Prod["API production yang sesuai kontrak"]

Gambar 7. Dari kontrak ke implementasi: spec memandu handler, dan CI menjaga keduanya selaras.

Yang Wajib Menempel

  • OpenAPI adalah kontrak HTTP formal yang machine-readable, jadi single source of truth lintas backend, frontend, mobile, QA, dan integrasi luar.
  • Course memakai OpenAPI 3.1.1 sebagai default; ia selaras penuh dengan JSON Schema 2020-12 dan kompatibel dengan tooling luas.
  • Struktur dokumen: openapi, info, servers, paths, components, security. paths adalah jantungnya, components adalah lemari reusable lewat $ref.
  • Pilih HTTP method sesuai semantiknya (safe dan idempotent), jangan POST untuk segalanya.
  • Parameter punya tiga lokasi: path (identitas), query (filter dan pagination), header (metadata seperti Idempotency-Key).
  • Response harus menutup error, bukan hanya 200: 201, 400, 401, 404, 409, 422, dan 500.
  • Uang disimpan sebagai integer rupiah, bukan float. Di Go, PriceRupiah int64.
  • Error standard yang konsisten (atau Problem Details RFC 9457) menghemat tebak-tebakan client.
  • Security: definisikan securitySchemes, terapkan global, override per operasi (security: [] untuk publik).

Langkah berikutnya jelas: dari menulis spec, kamu masuk ke implementasi. Bangun handler dengan net/http dan chi, tegakkan validasi sesuai schema, lalu pasang contract check di CI agar kode dan kontrak tidak pernah menyimpang. Untuk mendalami codegen dan integrasi Go, lihat oapi-codegen. Dengan kontrak yang kokoh sebagai fondasi, sisa jalur Web Artisan, dari routing sampai deploy ke AWS, berdiri di atas dasar yang sudah disepakati bersama.

Progress disimpan lokal di browser ini.