Frontend vs Backend: API Contract Drift

Pemrograman sistem informasi pada masa modern ini tidak lagi menggunakan konsep monolith—Tampilan, logika bisnis dan database dalam satu Codebase. Pemisahan fungsi dan fokus menjadikan lebih efektif, efesien dan extendable.

Dari pada menyatukan kode tampilan dan logika bisnis dalam satu kode, lebih baik dipisah menjadi Frontend sebagai kode mengurus tampilan responsif dan reaktif, dan Backend sebagai bagian berkomunikasi ke Database, Jaringan, Cache dan logika bisnis.

Kontrak API

Frontend dan Backend perlu berkomunikasi menggunakan sebuah bahasa yang sama-sama disetujui, yaitu Kontrak API dalam bentuk RESTFull API.

Kontrak API (eng: API Contracts) jadi pegangan bagi keduanya untuk menyesuaikan ekspektasi struktur data yang diterima dan dikirim.

Frontend vs Backend— Contoh Masalah API Contract Drift

Masalah sederhana yang sering diangkat di sosial media adalah perbedaan nama variabel backend dan frontend seperti video reels ini.

Perubahan struktur data sering terjadi di backend antara karena kebijakan backend, perubahan nama kolom database, atau integrasi dari sistem lain, tetapi perubahan ini membuat frontend jadi error.

Sebagai contoh kode frontend berikut mengambil data dan menampilkannya di beberapa komponen UI:

// Frontend mengambil data dari Backend
const user = await fetch('http://backend/user').then(res => res.json())

// tampilkan di tampilan 
const component1 = (
  <div>{user.email}</div>
  <div>{user.name}</div>
  <div>{user.phone}</div>
)

const component2 = (
  <div>Hi, selamat datang {user.name}</div>
)

const component3 = (
  <div>Diskon untuk {user.name} telah diterapkan</div>
)

Pada sebuah situasi, tiba-tiba backend melakukan perubahan dari name menjadi fullname. Maka Frontend harus mengganti seluruh komponen yang menggunakan kode tersebut menjadi struktur yang benar.

// Frontend mengambil data dari Backend 
const user = await fetch('http://backend/user').then(res => res.json())

// tampilkan di tampilan 
const component1 = (
  <div>{user.email}</div>
-  <div>{user.name}</div>
+  <div>{user.fullname}</div>
  <div>{user.phone}</div>
)

const component2 = (
-  <div>Hi, selamat datang {user.name}</div>
+  <div>Hi, selamat datang {user.fullname}</div>
)

const component3 = (
-  <div>Diskon untuk {user.name} telah diterapkan</div>
+  <div>Diskon untuk {user.fullname} telah diterapkan</div>
)

Maka cara ini sangat tidak efektif, karena:

  • Mengubah berbagai file dan komponen
  • Perlu uji ulang pada berbagai area
  • Bug tidak tertlihat atau terabaikan
  • Durasi pengerjaan jadi lebih lama

Diperlukan cara elegan untuk mengatasi perubahan ini.

Solusi #1: Frontend Membuat Bridge

Dari contoh kode diatas, struktur data frontend sangat bergantung pada struktur data Backend—disebut Tight Coupling.

Bridge adalah structural design pattern untuk memisahkan kode menjadi abstraction (Bagaimana data digunakan) dan implementation (Bagaimana data diambil). Konsep ini dipinjam dari OOP (Object Oriented Programming).

Bridge memberikan jembatan untuk mengatasi perubahan dari sisi Implementation atau abstraction, jika API berubah maka ubah implementation, jika UI berubah maka ubah abstraction.

Berikut kode yang telah dimodifikasi:

// ── IMPLEMENTATION (how data is fetched) ──────────────────────
const UserImplementation = {
  fetchUser: async () => {
    return await fetch('http://backend/user').then(res => res.json())
  }
}

// ── ABSTRACTION (how data is used) ────────────────────────────
const UserAbstraction = (impl) => ({
  getUser: () => impl.fetchUser()
})

// ── Bridge them together ──────────────────────────────────────
const userBridge = UserAbstraction(UserImplementation)
const user = await userBridge.getUser()

// ── Components stay the same ──────────────────────────────────
const component1 = (
  <div>{user.email}</div>
  <div>{user.name}</div>
  <div>{user.phone}</div>
)

const component2 = (
  <div>Hi, selamat datang {user.name}</div>
)

const component3 = (
  <div>Diskon untuk {user.name} telah diterapkan</div>
)

Apabila ada perubahan dari API cukup ubah kode di Abstraction.

...
// ── ABSTRACTION (how data is used) ────────────────────────────
const UserAbstraction = (impl) => ({
-  getUser: () => impl.fetchUser()
+  getUser: () => impl.fetchUser().then(data => ({
+    ...data,
+    name: data.fullname || data.name
   })
})
...

Dengan begitu, tidak perlu melakukan perubahan pada seluruh komponen karena Implementasion masih mengeluarkan struktur yang sama.

Kelebihan lain dari Bridge adalah kamu bisa membuat beberapa Implementation lainnya, misalnya Mock—contoh sampel data bertujuan untuk pengujian.

// ── IMPLEMENTATION (how data is fetched) ──────────────────────
const UserImplementation = {...}
const MockImplementation = {
  fetchUser: async () => {
    return Promise.resolve({ id: 'test-1', name: "John doe", email: "[email protected]"})
  }
}
...
// ── Bridge them together ──────────────────────────────────────
const userBridge = UserAbstraction(MockImplementation) // Gunakan Mock
const user = await userBridge.getUser()

Dengan begitu, kamu bisa dengan mudah mengganti implementation dari Mock ke API apabila server sudah tersedia.

Solusi #2: Repository Pattern

Cara kedua adalah menggunakan konsep Repository pattern. Mengambil konsep di Backend yang menggunakan Repository untuk berkomunikasi dengan Database.

Analogi Repository: Jika kamu ke perpustakaan, kamu tidak langsung ke ruangan buku untuk cari buku, kamu minta pustakawan untuk bantu cari buku karena dia tahu dimana lokasi buku.

Dengan cara yang sama, Repository menjadi sebuah class yang bertugas untuk mengambil data dari API—dalam kasus frontend.

Repository dapat ditulis sebagai class sederhana dengan method yang dibutuhkan. Berikut kode yang telah dimodifikasi

// ── REPOSITORY ────────────────────────────────────────────────
const UserRepository = {
  getUser: async () => {
    return await fetch('http://backend/user').then(res => res.json())
  }
}

// ── Components consume the repository, not fetch directly ─────
const user = await UserRepository.getUser()

const component1 = (
  <div>{user.email}</div>
  <div>{user.name}</div>
  <div>{user.phone}</div>
)

const component2 = (
  <div>Hi, selamat datang {user.name}</div>
)

const component3 = (
  <div>Diskon untuk {user.name} telah diterapkan</div>
)

Perubahan API dapat diatasi dengan update kode repository saja.

// ── REPOSITORY ────────────────────────────────────────────────
const UserRepository = {
  getUser: async () => {
-    return await fetch('http://backend/user').then(res => res.json())
+    const res = await fetch('http://backend/user').then(res => res.json())
+    return {
+      ...res, 
+      name: res.fullname || res.name
+    }
  }
}

Pendekatan Repository lebih sederhana dibanding Bridge karena hanya membuat class Repository yang berisi implementasi API.

Tetapi Repository terbatas pada satu jenis Implementation dan tidak extendable seperti bridge.

Bridge atau Repository?

Contoh paling cocok menggunakan Bridge adalah saat memulai proyek. Frontend membuat Implementation Mock untuk pengujian, ketika API sudah siap dipakai, frontend cukup membuat Implementation API Baru.

Apabila proyek kamu sudah berjalan maka gunakan repository karena metode yang sederhana dan terpusat. Kamu akan mudah migrasi karena cukup panggil repository yang dibutuhkan.

Less Fight, More Productive

Pembuatan software tidak hanya tentang menuliss kode, Integrasi dengan berbagai sistem baik itu internal maupun external membutuhkan keputusan pemilihan struktur kode yang baik agar projek menjadi scalable dan extendable.

Kecuali kamu membuat prototype, kamu ingin cepat selesai untuk menguji ide dan eksperimen, maka cara apapun yang kamu tahu itu lebih baik. Malah cara diatas mungkin akan menghambat pembuatan prototype.

Semoga tulisan ini membantu dan bermanfaat, terima kasih sudah membaca. Klik Subscribe untuk dapatkan tips dan trik lainnya.

Subscribe now!