Dokumentasi adalah wajah API Anda bagi dunia luar — dan selama dokumentasi itu berbentuk prosa bebas, ia akan berbohong begitu kode berubah. OpenAPI mengatasinya dengan satu dokumen terstruktur yang mendeskripsikan seluruh endpoint, parameter, respons, dan skema data dalam format yang bisa dibaca mesin. Dari dokumen ini lahir dokumentasi interaktif, SDK klien, hingga test sinkronisasi. Pada Chapter ini kita bedah anatomi berkas OpenAPI 3, membandingkan spec tulisan tangan dengan yang dibangkitkan otomatis, lalu menutup dengan test sederhana yang memastikan router dan dokumentasi tidak pernah saling meninggalkan.
Anatomi Berkas OpenAPI #
Dokumen OpenAPI dapat ditulis dalam YAML atau JSON — keduanya sah dan setara. Bagian atas berkas selalu memuat identitas spec dan API yang dideskripsikan:
openapi: 3.1.0
info:
title: TaskApp API
version: 1.0.0
description: API manajemen task untuk aplikasi TaskApp.
servers:
- url: https://api.taskapp.example.com
description: Server produksi
paths:
/tasks:
get:
summary: Daftar task
responses:
"200":
description: Daftar task beserta meta paginasiEmpat bagian teratas punya peran yang tegas:
openapi: versi spesifikasi OpenAPI yang dipakai dokumen —3.1.0di sini. Versi spec menentukan fitur bahasa yang boleh dipakai, jadi jangan tertukar dengan versi API Anda sendiri.info: metadata API — judul, deskripsi, danversion. Nilaiversioninilah tempat yang tepat untuk menuliskan versi API dari Chapter API Versioning , sehingga kontrak tiap versi terdokumentasi terpisah.servers: daftar base URL — produksi, staging, atau localhost — tempat path-path di bawahnya ditempelkan.paths: peta endpoint; setiap path memuat operasi per HTTP method lengkap dengan parameter, body, dan responsnya.
Perhatikan bahwa status code ditulis sebagai string ("200") — dalam JSON, kunci objek memang selalu string, dan YAML mengikuti konvensi yang sama.
Mendokumentasikan Endpoint, Respons, dan Skema #
Operasi yang lebih kaya membutuhkan tiga bagian tambahan: parameter untuk input di URL, requestBody untuk input di body, dan components untuk definisi skema yang bisa dipakai ulang:
paths:
/tasks:
post:
summary: Buat task baru
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TaskInput"
responses:
"201":
description: Task berhasil dibuat
"422":
description: Request tidak lolos validasi
content:
application/json:
schema:
$ref: "#/components/schemas/ValidationError"
/tasks/{id}:
get:
summary: Ambil detail satu task
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Detail task
"404":
description: Task tidak ditemukan
components:
schemas:
TaskInput:
type: object
required: [title]
properties:
title:
type: string
maxLength: 100
assignee:
type: string
format: email
ValidationError:
type: object
required: [code, message, details]
properties:
code:
type: string
example: VALIDATION_ERROR
message:
type: string
details:
type: array
items:
type: object
properties:
field:
type: string
rule:
type: string
message:
type: stringKunci $ref adalah mekanisme pakai-ulang OpenAPI: #/components/schemas/TaskInput menunjuk definisi di dalam dokumen yang sama, sehingga satu skema bisa dipakai di banyak operasi tanpa duplikasi — pola DRY yang sama seperti constant di kode. Parameter path dideklarasikan dengan in: path, sedangkan parameter query dari Chapter Pagination, Filtering, dan Sorting
ditandai in: query. Skema ValidationError sengaja menerjemahkan bentuk respons 422 dari Chapter Request Validation
— field, rule, message — sehingga konsumen API tahu persis bentuk error yang akan diterimanya.
Spec Tulisan Tangan atau Otomatis #
Ada dua kubu alur kerja: menulis spec lebih dulu dengan tangan, atau membangkitkannya dari kode yang sudah ada. Keduanya sah, dengan konsekuensi berbeda:
| Aspek | Tulisan tangan (spec-first) | Dibangkitkan dari kode |
|---|---|---|
| Kendali bentuk akhir | Penuh — spec menjadi kontrak resmi sejak awal | Mengikuti implementasi apa adanya |
| Cocok untuk | API publik, tim terpisah frontend-backend | Prototype dan proyek internal cepat |
| Risiko utama | Basi ketika lupa diperbarui | Detail sulit dikontrol, anotasi menyebar di seluruh kode |
| Alat bantu | Editor dan linter spec | Generator dari anotasi, mis. swagger-php |
Kubu generator memakai alat seperti swagger-php: anotasi pada class dan method dipindai, lalu berkas spec dihasilkan otomatis. Kubu spec-first justru memperlakukan dokumen sebagai sumber kebenaran: spec ditinjau lebih dulu, baru kode mengikutinya. Untuk API yang dikonsumsi pihak luar, spec-first hampir selalu lebih sehat karena kontrak bisa disepakati sebelum satu baris handler ditulis.
Pratinjau dengan Swagger UI dan Redoc #
Spec yang tersimpan sebagai berkas statis bisa langsung ditampilkan menjadi dokumentasi yang rapi. Swagger UI membacanya dari sisi browser lalu merender daftar endpoint yang bisa dikembangkan; Redoc menawarkan tampilan baca-saja yang lebih tenang untuk publikasi. Keduanya hanya butuh satu halaman HTML yang memuat pustaka dari CDN dan menunjuk berkas spec Anda:
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Dokumentasi API TaskApp</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
window.onload = () => SwaggerUIBundle({
url: "openapi.json",
dom_id: "#swagger-ui",
});
</script>
</body>
</html>Karena seluruhnya berkas statis, halaman semacam ini bisa dihosting di mana saja tanpa backend PHP tambahan — cukup pastikan openapi.json terbarui setiap kali spec berubah. Kombinasi yang umum: Redoc untuk dokumentasi publik yang rapi, Swagger UI untuk lingkungan development yang butuh eksplorasi cepat.
Validasi Spec dan Alur Contract-First #
Sebelum spec dibaca orang lain, kesalahan strukturnya perlu ditangkap lebih dulu. Swagger Editor menyoroti kesalahan sambil Anda menulis; untuk pemeriksaan berulang di CI, linter seperti Redocly CLI memeriksa spec dari baris perintah dan menolak merge yang melanggar aturan — misalnya operasi tanpa responses, atau penamaan path yang tidak konsisten. Aturan mainnya sama seperti PHP: kesalahan ketik harus ketahuan oleh mesin, bukan oleh pembaca.
Alur contract-first yang sehat berbentuk lingkaran:
- Tulis (atau ubah) spec dan tinjau bersama konsumen API — kontrak disepakati sebelum kode.
- Validasi spec dengan linter sampai bersih.
- Implementasikan router dan handler mengikuti spec.
- Jalankan test sinkronisasi spec–router (contoh di bawah) sebagai bagian dari CI.
- Terbitkan spec dan halaman pratinjau sebagai dokumentasi resmi.
Menjaga Spec Sinkron dengan Test #
Musuh utama dokumentasi adalah drift: endpoint baru ditambahkan di router, spec tidak ikut diperbarui, dan dokumentasi mulai berbohong pelan-pelan. Pertahanannya adalah test yang membandingkan kedua dunia — daftar route dari router versus daftar operasi dari spec. Berikut versi ringkasnya; spec JSON ditanam sebagai string agar contoh berdiri sendiri (di proyek nyata ia dibaca dari berkas openapi.json di repositori):
<?php
$specJson = <<<'JSON'
{
"openapi": "3.1.0",
"info": {
"title": "TaskApp API",
"version": "1.0.0",
"description": "API manajemen task untuk aplikasi TaskApp."
},
"paths": {
"/tasks": {
"get": {
"summary": "Daftar task",
"parameters": [
{
"name": "status",
"in": "query",
"schema": {"type": "string", "enum": ["todo", "doing", "done"]}
},
{
"name": "page",
"in": "query",
"schema": {"type": "integer", "minimum": 1, "default": 1}
}
],
"responses": {
"200": {"description": "Daftar task beserta meta paginasi"}
}
},
"post": {
"summary": "Buat task baru",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/TaskInput"}
}
}
},
"responses": {
"201": {"description": "Task berhasil dibuat"},
"422": {"description": "Request tidak lolos validasi"}
}
}
},
"/tasks/{id}": {
"get": {
"summary": "Ambil detail satu task",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {"type": "integer"}
}
],
"responses": {
"200": {"description": "Detail task"},
"404": {"description": "Task tidak ditemukan"}
}
}
}
},
"components": {
"schemas": {
"TaskInput": {
"type": "object",
"required": ["title"],
"properties": {
"title": {"type": "string", "maxLength": 100},
"assignee": {"type": "string", "format": "email"}
}
}
}
}
}
JSON;
$routes = [
"GET /tasks",
"POST /tasks",
"GET /tasks/{id}",
"DELETE /tasks/{id}",
];
function lintRoutes(string $specJson, array $routes): array
{
$spec = json_decode($specJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return ["Spec bukan JSON valid: " . json_last_error_msg()];
}
$problems = [];
$specRoutes = [];
foreach ($spec["paths"] ?? [] as $path => $operations) {
foreach ($operations as $method => $operation) {
if (!is_array($operation)) {
continue;
}
if (!isset($operation["responses"])) {
$problems[] = "Operasi " . strtoupper($method)
. " $path tidak mendokumentasikan responses";
}
$specRoutes[] = strtoupper($method) . " " . $path;
}
}
foreach (array_diff($routes, $specRoutes) as $route) {
$problems[] = "Route $route tidak terdokumentasi di spec";
}
foreach (array_diff($specRoutes, $routes) as $route) {
$problems[] = "Spec mendokumentasikan $route tetapi router tidak memilikinya";
}
return $problems;
}
$problems = lintRoutes($specJson, $routes);
echo "Masalah ditemukan: " . count($problems) . "\n";
foreach ($problems as $problem) {
echo "- $problem\n";
}Output yang Dihasilkan:
Masalah ditemukan: 1
- Route DELETE /tasks/{id} tidak terdokumentasi di specMari kita bedah cara kerjanya:
json_decode+json_last_error(): spec diurai menjadi array asosiatif lebih dulu; jika JSON-nya rusak, fungsi langsung melaporkan masalah parsing tanpa memproses lebih jauh — persis pola dari Chapter Request Parsing .- Loop ganda
paths: setiap path memuat operasi per method (get,post, dan kawan-kawan); setiap operasi diubah menjadi kunci"METHOD /path"yang formatnya sama dengan tabel$routesdari Chapter Routing dan Endpoints . array_diff()dua arah: selisih router−spec menangkap endpoint yang belum terdokumentasi, selisih spec−router menangkap dokumentasi yang menjanjikan endpoint hantu.- Pemeriksaan
responses: operasi tanpa blokresponsesditandai — kelalaian paling umum saat menulis spec terburu-buru.
Pada contoh ini, route DELETE /tasks/{id} ada di router tetapi belum terdokumentasi, sehingga test melaporkannya. Perbaikannya sederhana: tambahkan operasi delete di bawah /tasks/{id} pada spec, dan test kembali hijau. Bungkus pemanggilan lintRoutes() dalam test PHPUnit yang membaca berkas spec dari repositori, dan setiap perubahan router yang lupa dokumentasi akan menggagalkan build sebelum sempat mencapai produksi. Berkas openapi.json pada bagian bawah halaman berisi spec lengkap yang dipakai contoh ini — unduh, jalankan lewat linter, lalu buka di Swagger UI untuk melihat hasilnya.
Ringkasan #
OpenAPI mengubah dokumentasi dari prosa menjadi kontrak terstruktur: info dan servers memperkenalkan API, paths mendeskripsikan operasi, dan components merangkum skema yang dipakai ulang lewat $ref. Spec-first memberi kendali penuh atas kontrak, pratinjau Swagger UI dan Redoc menghidupkannya, dan test lintRoutes() menjaganya tetap jujur terhadap router. Dengan kontrak yang terdokumentasi dan teruji, API Anda siap memasuki wilayah operasional — pembatasan laju request dan strategi idempotency, yang membangun langsung di atas fondasi section ini.
Referensi #
- OpenAPI Specification 3.1.0 — situs resmi OpenAPI Initiative
- Swagger — dokumentasi OpenAPI resmi
- json_decode — dokumentasi resmi PHP
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
openapi.json
Unduh berkas{
"openapi": "3.1.0",
"info": {
"title": "TaskApp API",
"version": "1.0.0",
"description": "API manajemen task untuk aplikasi TaskApp."
},
"paths": {
"/tasks": {
"get": {
"summary": "Daftar task",
"parameters": [
{
"name": "status",
"in": "query",
"schema": {
"type": "string",
"enum": ["todo", "doing", "done"]
}
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer",
"minimum": 1,
"default": 1
}
}
],
"responses": {
"200": {
"description": "Daftar task beserta meta paginasi"
}
}
},
"post": {
"summary": "Buat task baru",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskInput"
}
}
}
},
"responses": {
"201": {
"description": "Task berhasil dibuat"
},
"422": {
"description": "Request tidak lolos validasi"
}
}
}
},
"/tasks/{id}": {
"get": {
"summary": "Ambil detail satu task",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Detail task"
},
"404": {
"description": "Task tidak ditemukan"
}
}
}
}
},
"components": {
"schemas": {
"TaskInput": {
"type": "object",
"required": ["title"],
"properties": {
"title": {
"type": "string",
"maxLength": 100
},
"assignee": {
"type": "string",
"format": "email"
}
}
}
}
}
}