Desain API menetapkan model sumber daya, daftar endpoint, serta kontrak respons dan error sebelum kode Express ditulis. Topik ini memodelkan dua sumber daya, yaitu pengguna dan catatan, dengan relasi satu-ke-banyak, lalu menyusun endpoint publik dan terlindungi, kemudian menetapkan bentuk respons sukses dan respons error yang konsisten. Spesifikasi OpenAPI menjadi dokumen yang mengikat kontrak tersebut, sehingga implementasi dan pengujian tinggal mengikuti peta yang sudah ditetapkan.
Model sumber daya dan relasi#
Model sumber daya layanan catatan terdiri atas dua entitas, yaitu pengguna dan catatan. Pengguna memiliki atribut id, email, kataSandi, nama, dan dibuatPada. Catatan memiliki atribut id, judul, isi, tag, penggunaId, dibuatPada, dan diperbaruiPada. Atribut penggunaId pada catatan adalah kunci asing yang merujuk id pengguna, sehingga relasi satu-ke-banyak terjalin antara pengguna dan catatan. Email pengguna bersifat unik karena dipakai sebagai kredensial login.
Atribut tag pada catatan disimpan sebagai larik string karena tag bersifat opsional dan banyak. Pilihan ini menghindarkan tabel relasi tambahan untuk tag, sebab pencarian tag tidak menjadi kasus utama layanan. Atribut dibuatPada dan diperbaruiPada diisi otomatis oleh basis data, masing-masing dengan DEFAULT NOW() dan ON UPDATE NOW() pada PostgreSQL, sehingga aplikasi tidak perlu menetapkan nilai keduanya secara manual.
Relasi pada diagram di atas berarti setiap catatan dimiliki tepat oleh satu pengguna, sedangkan satu pengguna dapat memiliki nol atau banyak catatan. Konsekuensi praktis relasi ini adalah setiap kueri catatan harus difilter berdasarkan penggunaId pengguna terotentikasi, sehingga pengguna A tidak dapat membaca catatan milik pengguna B. Filter WHERE "penggunaId" = ? menjadi klausa wajib pada seluruh kueri catatan, bukan opsional.
Endpoint dan method#
Endpoint disusun menjadi dua kelompok, yaitu publik dan terlindungi. Kelompok publik mencakup registrasi dan login, sedangkan kelompok terlindungi mencakup seluruh operasi catatan. Pemisahan kelompok ini tercermin pada struktur berkas rute, yaitu routes/auth.js untuk rute publik dan routes/catatan.js untuk rute terlindungi yang dipasang di bawah middleware auth.
| Method | Jalur | Otentikasi | Deskripsi |
|---|---|---|---|
| POST | /auth/register | publik | Registrasi pengguna baru |
| POST | /auth/login | publik | Login dan dapatkan JWT |
| GET | /catatan | JWT | Daftar catatan milik pengguna |
| POST | /catatan | JWT | Buat catatan baru |
| GET | /catatan/:id | JWT | Ambil satu catatan |
| PUT | /catatan/:id | JWT | Perbarui catatan |
| DELETE | /catatan/:id | JWT | Hapus catatan |
Pilihan method mengikuti konvensi REST. POST dipakai untuk pembuatan sumber daya karena badan permintaan membawa data baru. GET tanpa :id mengembalikan daftar, sedangkan GET dengan :id mengembalikan satu sumber daya. PUT dipakai untuk pembaruan menyeluruh, sedangkan DELETE menghapus sumber daya. Pemilihan PUT alih-alih PATCH di proyek ini menyederhanakan implementasi sebab seluruh bidang wajib dikirim ulang pada pembaruan, sehingga validasi memakai schema yang sama dengan pembuatan.
Penamaan jalur memakai kata benda jamak untuk koleksi (/catatan) dan parameter :id untuk satu sumber daya. Bahasa Indonesia tidak memiliki perubahan bentuk jamak yang eksplisit, sehingga /catatan dapat berarti satu atau banyak tergantung method. Verba tidak dimasukkan pada jalur, sehingga POST /catatan/buat dihindari, sebab method POST pada /catatan sudah menyatakan pembuatan.
Kontrak respons dan error#
Kontrak respons menetapkan dua bentuk, yaitu respons sukses dan respons error. Respons sukses selalu membungkus data pada properti data, sedangkan respons daftar menambahkan properti meta yang memuat total, halaman, dan perHalaman. Respons error selalu membungkus detail pada properti error yang memuat kode, pesan, dan opsional detail sebagai larik pasangan bidang dan pesan. Pemisahan dua bentuk ini membuat klien dapat memeriksa keberhasilan respons hanya dengan melihat adanya properti data atau error.
// tidak-dijalankan: eksekusi delegasi ke blok bash berikut agar keluaran terkonsolidasi di satu tempat
const responsSukses = {
data: {
id: 42,
judul: "Belajar Node.js minggu pertama",
isi: "Catatan tentang modul inti dan stream.",
tag: ["node", "stream"],
penggunaId: 7,
dibuatPada: "2026-11-02T08:30:00.000Z",
diperbaruiPada: "2026-11-02T08:30:00.000Z",
},
};
const responsDaftar = {
data: [responsSukses.data],
meta: { total: 1, halaman: 1, perHalaman: 20 },
};
const responsError = {
error: {
kode: "MASUKAN_TIDAK_VALID",
pesan: "Body permintaan tidak lolos validasi",
detail: [
{ bidang: "judul", pesan: "Judul minimal 3 karakter" },
{ bidang: "isi", pesan: "Isi tidak boleh kosong" },
],
},
};
console.log("--- Respons sukses (201 Created) ---");
console.log(JSON.stringify(responsSukses, null, 2));
console.log("\n--- Respons daftar (200 OK) ---");
console.log(JSON.stringify(responsDaftar, null, 2));
console.log("\n--- Respons error (400 Bad Request) ---");
console.log(JSON.stringify(responsError, null, 2));# jalankan dari direktori topik
node respons-contoh.js--- Respons sukses (201 Created) ---
{
"data": {
"id": 42,
"judul": "Belajar Node.js minggu pertama",
"isi": "Catatan tentang modul inti dan stream.",
"tag": [
"node",
"stream"
],
"penggunaId": 7,
"dibuatPada": "2026-11-02T08:30:00.000Z",
"diperbaruiPada": "2026-11-02T08:30:00.000Z"
}
}
--- Respons daftar (200 OK) ---
{
"data": [
{
"id": 42,
"judul": "Belajar Node.js minggu pertama",
"isi": "Catatan tentang modul inti dan stream.",
"tag": [
"node",
"stream"
],
"penggunaId": 7,
"dibuatPada": "2026-11-02T08:30:00.000Z",
"diperbaruiPada": "2026-11-02T08:30:00.000Z"
}
],
"meta": {
"total": 1,
"halaman": 1,
"perHalaman": 20
}
}
--- Respons error (400 Bad Request) ---
{
"error": {
"kode": "MASUKAN_TIDAK_VALID",
"pesan": "Body permintaan tidak lolos validasi",
"detail": [
{
"bidang": "judul",
"pesan": "Judul minimal 3 karakter"
},
{
"bidang": "isi",
"pesan": "Isi tidak boleh kosong"
}
]
}
}Keluaran memperlihatkan tiga bentuk respons yang akan dikirim oleh implementasi pada topik berikutnya. responsSukses adalah bentuk 201 Created saat catatan baru dibuat, dengan data berisi catatan lengkap termasuk id yang dihasilkan server dan penggunaId yang diambil dari klaim JWT. responsDaftar adalah bentuk 200 OK untuk GET /catatan, dengan data sebagai larik dan meta menyatakan pagination. responsError adalah bentuk 400 Bad Request saat validasi gagal, dengan detail yang dipetakan dari error.issues zod.
Kontrak error memakai kode string alih-alih angka HTTP, sebab kode string dapat membedakan jenis error dengan lebih kaya daripada kode status. Kode MASUKAN_TIDAK_VALID, KREDENSIAL_SALAH, TIDAK_TEROTENTIKASI, TIDAK_DIIZINKAN, TIDAK_DITEMUKAN, dan KESALAHAN_INTERNAL mencakup kategori error yang umum pada layanan ini. Setiap kode dipetakan ke kode status HTTP yang sesuai, misalnya MASUKAN_TIDAK_VALID ke 400, TIDAK_TEROTENTIKASI ke 401, TIDAK_DIIZINKAN ke 403, TIDAK_DITEMUKAN ke 404, dan KESALAHAN_INTERNAL ke 500.
Spesifikasi OpenAPI#
Spesifikasi OpenAPI 3.1 mengikat kontrak di atas ke dalam satu berkas yang dapat dipakai oleh generator kode, klien pengujian, dan UI dokumentasi. Berkas openapi.yaml menyatakan info yang memuat nama dan versi layanan, servers yang memuat URL dasar, components yang memuat skema JSON yang dipakai ulang, dan paths yang memuat definisi tiap endpoint. Skema pada components/schemas menyatakan bentuk Catatan, CatatanInput, dan Error sebagai objek yang dirujuk oleh respons dan permintaan.
# tidak-dijalankan: spesifikasi deklaratif, bukan kode yang dieksekusi
openapi: 3.1.0
info:
title: Catatan API
version: 1.0.0
description: Layanan API catatan pribadi dengan autentikasi JWT.
servers:
- url: http://localhost:3000
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Catatan:
type: object
required: [id, judul, isi, tag, penggunaId, dibuatPada, diperbaruiPada]
properties:
id:
type: integer
example: 42
judul:
type: string
minLength: 3
maxLength: 120
example: Belajar Node.js minggu pertama
isi:
type: string
minLength: 1
tag:
type: array
maxItems: 5
items:
type: string
penggunaId:
type: integer
dibuatPada:
type: string
format: date-time
diperbaruiPada:
type: string
format: date-time
CatatanInput:
type: object
required: [judul, isi]
properties:
judul:
type: string
minLength: 3
maxLength: 120
isi:
type: string
minLength: 1
tag:
type: array
maxItems: 5
items:
type: string
Error:
type: object
required: [error]
properties:
error:
type: object
required: [kode, pesan]
properties:
kode:
type: string
example: MASUKAN_TIDAK_VALID
pesan:
type: string
example: Body permintaan tidak lolos validasi
detail:
type: array
items:
type: object
properties:
bidang:
type: string
pesan:
type: string
paths:
/auth/register:
post:
summary: Registrasi pengguna baru
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, kataSandi, nama]
properties:
email:
type: string
format: email
kataSandi:
type: string
minLength: 8
nama:
type: string
minLength: 2
responses:
'201':
description: Pengguna dibuat
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id:
type: integer
email:
type: string
nama:
type: string
'400':
description: Masukan tidak valid
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/auth/login:
post:
summary: Login dan dapatkan JWT
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, kataSandi]
properties:
email:
type: string
format: email
kataSandi:
type: string
responses:
'200':
description: Token JWT
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
token:
type: string
'401':
description: Kredensial salah
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/catatan:
get:
summary: Daftar catatan milik pengguna terotentikasi
security:
- bearerAuth: []
responses:
'200':
description: Daftar catatan
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Catatan'
meta:
type: object
properties:
total:
type: integer
halaman:
type: integer
perHalaman:
type: integer
'401':
description: Token tidak sah
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
summary: Buat catatan baru
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CatatanInput'
responses:
'201':
description: Catatan dibuat
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Catatan'
'400':
description: Masukan tidak valid
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/catatan/{id}:
get:
summary: Ambil satu catatan
security:
- bearerAuth: []
parameters:
- in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: Satu catatan
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Catatan'
'404':
description: Catatan tidak ditemukan
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
put:
summary: Perbarui catatan
security:
- bearerAuth: []
parameters:
- in: path
name: id
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CatatanInput'
responses:
'200':
description: Catatan diperbarui
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Catatan'
'404':
description: Catatan tidak ditemukan
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
summary: Hapus catatan
security:
- bearerAuth: []
parameters:
- in: path
name: id
required: true
schema:
type: integer
responses:
'204':
description: Catatan dihapus
'404':
description: Catatan tidak ditemukan
content:
application/json:
schema:
$ref: '#/components/schemas/Error'Berkas openapi.yaml di atas memakai $ref untuk merujuk skema pada components/schemas, sehingga Catatan dan CatatanInput didefinisikan sekali dan dipakai ulang pada beberapa endpoint. Pendekatan ini menghindari duplikasi dan menjaga perubahan skema, misalnya menambah bidang kategori, cukup dilakukan pada satu tempat. Skema keamanan bearerAuth menyatakan bahwa endpoint dengan security: - bearerAuth: [] memerlukan header Authorization: Bearer <token>, sehingga dokumentasi UI dapat menampilkan kunci input token dan pengujian otomatis dapat menyuntikkan JWT.
Berkas OpenAPI dapat diverifikasi memakai alat seperti swagger-cli validate openapi.yaml yang memeriksa kesesuaian dengan spesifikasi 3.1. Alat tersebut tidak tersedia di sandbox ini, sehingga verifikasi dilakukan dengan membaca struktur secara manual. Setiap paths memuat method, requestBody bila perlu, dan responses dengan kode status beserta skema konten. Skema yang dirujuk lewat $ref harus ada pada components/schemas, dan setiap $ref diawali dengan #/components/schemas/ yang merupakan jalur JSON Pointer ke dalam dokumen yang sama.
Kesalahan umum#
Mencampur verb HTTP pada jalur. Pola POST /catatan/buat dan POST /catatan/hapus/:id tampak eksplisit, tetapi melanggar konvensi REST yang memetakan method ke operasi. Pertahankan POST /catatan untuk buat dan DELETE /catatan/:id untuk hapus, sehingga klien yang familiar dengan REST dapat menebak endpoint tanpa membaca dokumentasi tambahan.
Mengembalikan error dengan struktur berbeda untuk setiap kode status. Suatu layanan yang mengembalikan { pesan: "..." } pada 400 tetapi { error: "..." } pada 401 memaksa klien menulis dua cabang penanganan. Tetapkan satu bentuk responsError dan pakai untuk seluruh kode status non-2xx, sehingga klien hanya memeriksa properti error.kode.
Menulis OpenAPI setelah implementasi selesai. Dokumentasi yang ditulis belakangan cenderung tidak sinkron dengan kode sebab detail kecil seperti panjang minimum string atau kode status sering berubah selama implementasi. Tulis OpenAPI bersamaan dengan schema zod, sehingga keduanya saling memverifikasi dan perubahan satu memaksa perubahan yang lain.
Ringkasan#
- Dua sumber daya, yaitu pengguna dan catatan, berelasi satu-ke-banyak dengan kunci asing
penggunaIdpada tabel catatan. - Tujuh endpoint dibagi menjadi publik (
/auth/register,/auth/login) dan terlindungi (/catatandan/catatan/:iduntukGET,POST,PUT,DELETE), dengan method yang mengikuti konvensi REST. - Kontrak respons memisahkan respons sukses (
datadan opsionalmeta) dari respons error (errordengankode,pesan, dan opsionaldetail), sehingga klien dapat memeriksa propertidataatauerror. - Spesifikasi OpenAPI 3.1 mengikat model sumber daya, endpoint, dan kontrak respons ke dalam satu berkas
openapi.yamlyang memakai$refuntuk menghindari duplikasi skema.
Referensi#
- OpenAPI Specification — Version 3.1.0 — OpenAPI Initiative
— Spesifikasi resmi OpenAPI 3.1 yang menjelaskan struktur
info,servers,components, danpaths. - Describing Parameters — Swagger Documentation — Panduan resmi Swagger untuk parameter path dan query pada spesifikasi OpenAPI.
- HTTP Semantics — RFC 9110 — IETF — RFC resmi yang mendefinisikan method HTTP, kode status, dan semantik permintaan serta respons.
- OpenAPI Tutorial — Swagger Documentation
— Pengantar resmi Swagger yang membahas langkah-langkah menyusun dokumen OpenAPI dari
infohinggapaths.