Desain API

Model sumber daya, endpoint, dan kontrak error.

Daftar isi

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 pengguna dan catatanPenggunaid : Int @idemail : String @uniquekataSandi : Stringnama : StringdibuatPada : DateTimeCatatanid : Int @idjudul : Stringisi : Stringtag : String[]penggunaId : Int @fk1NSatu pengguna memiliki nol atau banyak catatan
Relasi satu-ke-banyak antara pengguna dan catatan, dengan kunci asing penggunaId pada tabel catatan

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.

MethodJalurOtentikasiDeskripsi
POST/auth/registerpublikRegistrasi pengguna baru
POST/auth/loginpublikLogin dan dapatkan JWT
GET/catatanJWTDaftar catatan milik pengguna
POST/catatanJWTBuat catatan baru
GET/catatan/:idJWTAmbil satu catatan
PUT/catatan/:idJWTPerbarui catatan
DELETE/catatan/:idJWTHapus 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.

js respons-contoh.js
// 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));
bash
# jalankan dari direktori topik
node respons-contoh.js
text Keluaran
--- 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.

yaml openapi.yaml
# 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 penggunaId pada tabel catatan.
  • Tujuh endpoint dibagi menjadi publik (/auth/register, /auth/login) dan terlindungi (/catatan dan /catatan/:id untuk GET, POST, PUT, DELETE), dengan method yang mengikuti konvensi REST.
  • Kontrak respons memisahkan respons sukses (data dan opsional meta) dari respons error (error dengan kode, pesan, dan opsional detail), sehingga klien dapat memeriksa properti data atau error.
  • Spesifikasi OpenAPI 3.1 mengikat model sumber daya, endpoint, dan kontrak respons ke dalam satu berkas openapi.yaml yang memakai $ref untuk menghindari duplikasi skema.

Referensi#