# Satu Kontrak, Dua Backend: Paritas Offline untuk Web dan Mobile

> Bagaimana satu kontrak repository bertipe dengan adapter Api dan Mock memberi POS Next.js dan aplikasi Flutter kami paritas offline dan demo penuh.

URL: https://www.matthewswong.com/id/blog/mock-real-repository-adapter-offline-parity/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2026-07-14
- Updated: 2026-07-14
- Category: Software Development

> **Key takeaway:** Beri setiap layar satu kontrak repository bertipe, lalu kirim dua implementasi di baliknya: ApiRepository yang memanggil backend dan MockRepository yang didukung fixture. Tukar keduanya dengan satu flag konfigurasi. Anda mendapat demo langsung tanpa server, tes deterministik, dan pekerjaan frontend yang tidak pernah terhambat menunggu backend siap.

JID Carwash ERP berjalan di tiga permukaan: API NestJS 10 plus Prisma 5, POS dan panel admin Next.js 15, serta aplikasi lapangan Flutter 3.22 yang dibawa kru ke sekitar area cuci. Sejak awal saya menemui masalah yang membosankan tapi mahal. Pemilik ingin melihat POS di laptopnya saat rapat tanpa Wi-Fi. Rekan frontend saya ingin membangun layar payroll sebelum endpoint payroll ada. Dan saya ingin tes Flutter saya berjalan di CI tanpa menyalakan database Postgres. Ketiganya adalah masalah yang sama dengan topeng berbeda: UI seharusnya tidak peduli apakah backend nyata ada di ujung sana.

Solusinya adalah pola repository adapter yang diterapkan konsisten di kedua frontend. Setiap domain punya satu kontrak, dan dua implementasi yang dapat dipertukarkan berada di baliknya. Ini adalah abstraksi yang sama yang dipakai aplikasi Flutter clean-architecture untuk membuat lapisan domain tidak tahu-menahu soal sumber data, dan yang dipakai klien API TypeScript bertipe agar UI dikompilasi terhadap spesifikasi alih-alih endpoint langsung. Berikut persis cara kerjanya di basis kode yang sudah dikirim, termasuk jebakan yang membuat sebagian besar lapisan mock berbohong.

## Satu kontrak, didefinisikan sekali

Semuanya dimulai dari satu berkas tipe. Ia mendeklarasikan bentuk setiap entitas dan metode yang harus disediakan setiap repository. Kontrak adalah hukum yang dipatuhi kedua implementasi, dan yang terpenting, ia menyandikan invarian domain langsung di dalam tipe. Uang adalah bilangan bulat IDR, bukan angka pecahan, sehingga field-nya berupa number biasa yang dinamai agar satuannya jelas. Setiap panggilan daftar dan detail menerima branchId, karena di sisi server setiap query Prisma dibatasi per cabang untuk mencegah satu cabang membaca order cabang lain.

## Sisi Next.js: httpClient versus mockClient

Di web, kedua implementasi adalah objek biasa yang sama-sama memenuhi antarmuka OrdersRepository. Yang live membungkus klien HTTP kami dan memanggil rute NestJS. Yang mock membaca dari modul fixture di memori. Aturan yang saya tegakkan sejak hari pertama: mock harus mereproduksi perilaku yang dijamin API, bukan sekadar mengembalikan data. Jadi mock memfilter fixture berdasarkan branchId persis seperti server membatasi query-nya, dan metode settle-nya melakukan deduplikasi pada idempotency key persis seperti layanan settlement asli. Jika mock melewatkan itu, demo akan berperilaku berbeda dari produksi dan seluruh tujuannya runtuh.

## Sisi Flutter: ApiRepository versus MockRepository

Aplikasi lapangan mencerminkan ide yang persis sama di Dart, hanya dengan alat yang sesuai bahasanya. Setiap fitur mendeklarasikan abstract interface class, lalu mengirim dua kelas konkret. ApiOrdersRepository yang live berbicara ke NestJS dan, ketika perangkat offline, mengantrekan tulisannya ke outbox yang dikuras connectivity_plus begitu sinyal kembali. MockOrdersRepository menyimpan fixture di memori dan menjalankan baik demo maupun widget test. Riverpod merangkai pilihan itu di level provider, sehingga tidak ada widget yang pernah mengimpor repository konkret secara langsung; semuanya memantau provider dan menerima implementasi mana pun yang dipilih konfigurasi.

## Ditukar oleh konfigurasi, bukan oleh kode

Pertukarannya adalah satu boolean, diselesaikan sekali di composition root. Di web ia berupa variabel lingkungan yang dibaca saat build; di Flutter ia berupa field pada provider konfigurasi aplikasi yang diatur dari build flavor. Setiap komponen dan setiap layar mengimpor nama netral, bukan varian http atau mock. Satu lapisan tak langsung itulah yang membuat semuanya tanpa rasa sakit: untuk menjalankan demo tanpa Wi-Fi saya mengatur flag ke mock dan UI yang persis sama berjalan terhadap fixture; untuk mengirim produksi saya biarkan live. Tidak ada yang berubah di kode fitur.

> Mode kegagalannya adalah mock drift: fixture perlahan berhenti cocok dengan apa yang sebenarnya dikembalikan API, dan tes hijau menyembunyikan layar yang rusak. Dua aturan menjaga mock tetap jujur. Pertama, kedua implementasi harus memenuhi kontrak yang identik, sehingga field yang diganti nama merusak kompilasi di kedua sisi sekaligus. Kedua, mock harus menegakkan invarian yang sama seperti server, termasuk pembatasan cabang dan idempotensi, atau demo Anda berbohong kepada orang yang menontonnya.

## Apa yang sebenarnya pola ini berikan

- Demo tanpa backend. Pemilik menjalankan POS lengkap di laptopnya saat rapat tanpa jaringan, mencatat order dan menyelesaikan pembayaran terhadap fixture yang tampak persis seperti hari kerja cabang sungguhan.
- Frontend yang tidak pernah menunggu. Layar payroll dan kasbon dibangun dan ditinjau berminggu-minggu sebelum endpoint itu ada, dikodekan langsung terhadap mock dan beralih ke live begitu API dikirim.
- Tes cepat dan deterministik. Widget test Flutter dan component test Next.js berjalan terhadap fixture di CI tanpa Postgres, tanpa kerapuhan jaringan, dan hasil identik pada setiap eksekusi.
- Sandbox onboarding yang aman. Kontributor baru mengklona repo, mengatur flag ke mock, dan mendapat ERP yang berfungsi dalam satu perintah tanpa rahasia atau akses database apa pun.

> Hasilkan fixture Anda dari skrip seed yang sama yang mengisi cabang nyata, bukan secara manual. Milik kami memancarkan data fixture dalam bentuk persis seperti yang dikembalikan Prisma, sehingga data demo tak bisa dibedakan dari catatan cabang langsung. Fixture yang ditulis tangan adalah jalur tercepat menuju drift karena tidak ada yang ingat memperbaruinya.

## Api versus Mock sekilas

| Aspek | ApiRepository | MockRepository |
| --- | --- | --- |
| Sumber data | NestJS plus Prisma via HTTP | Fixture di memori, JSON hasil seed |
| Jaringan | Diperlukan, atau outbox offline | Tidak ada sama sekali |
| Tulisan | Dipersistkan, diantrekan di outbox jika offline | Bermutasi di memori, reset saat muat ulang |
| Cakupan cabang | Ditegakkan oleh query Prisma | Ditegakkan oleh filter fixture |
| Kegunaan utama | Produksi | Demo, tes, dev tanpa backend |

Pola ini tidak gratis. Anda memelihara implementasi kedua dari setiap repository, dan disiplin invarian di atas adalah pekerjaan nyata. Namun untuk produk dengan tiga permukaan yang berbagi satu domain, alternatifnya lebih buruk: UI yang terkopel ke backend langsung tidak bisa didemokan offline, tidak bisa dibangun mendahului API-nya, dan tidak bisa dites tanpa infrastruktur. Satu kontrak dan dua adapter membeli ketiganya, dan flag konfigurasi yang menukarnya adalah baris kode termurah di seluruh basis kode.

## Sources

- How to use Abstraction and the Repository Pattern in Flutter — Code With Andrea
- Offline-first support — Flutter architecture design patterns (official docs)
- TypeScript for API Contracts — Jon Mellman

## Source links

- https://codewithandrea.com/articles/abstraction-repository-pattern-flutter/
- https://docs.flutter.dev/app-architecture/design-patterns/offline-first
- https://www.jonmellman.com/posts/typescript-for-api-contracts/

## FAQ

### Apa itu pola repository adapter di aplikasi frontend?

Ini adalah desain di mana setiap domain data mendeklarasikan satu kontrak bertipe, dan dua atau lebih implementasi yang dapat dipertukarkan berada di baliknya. Komponen UI hanya bergantung pada kontrak, sehingga Anda bisa menukar implementasi HTTP langsung dengan mock berbasis fixture tanpa menyentuh layar apa pun. Pilihannya diselesaikan sekali di composition root.

### Bagaimana cara beralih antara repository mock dan nyata?

Dengan satu flag konfigurasi yang diselesaikan di composition root, bukan tersebar di seluruh kode. Di web ia berupa variabel lingkungan yang dibaca saat build; di Flutter ia berupa field pada config provider yang diatur dari build flavor. Setiap komponen mengimpor satu nama netral, jadi membalik flag menukar seluruh lapisan data sekaligus.

### Apakah repository mock harus cocok persis dengan backend?

Secara perilaku, ya. Kegagalan berbahaya adalah mock drift, di mana fixture berhenti cocok dengan respons API nyata dan tes hijau menyembunyikan layar rusak. Mock harus menegakkan invarian yang sama seperti server, seperti pembatasan cabang dan idempotensi, dan kedua implementasi harus memenuhi kontrak identik sehingga field yang diganti nama merusak kompilasi di kedua sisi.

### Kenapa tidak sekadar mengarahkan frontend ke backend staging saja?

Backend staging butuh jaringan, kredensial, dan infrastruktur yang berjalan, yang menggagalkan demo offline, menghambat pekerjaan frontend sebelum endpoint ada, dan membuat tes lambat serta rapuh. Mock berbasis fixture tidak punya ketergantungan itu. Ia berjalan di CI tanpa database dan membuat kontributor baru menyalakan aplikasi yang berfungsi dalam satu perintah.

### Apakah pola ini bisa bekerja lintas aplikasi web dan mobile?

Ya, dan di situlah ia paling menguntungkan. Di ERP carwash kami, POS Next.js memakai pasangan httpClient dan mockClient yang berbagi satu kontrak TypeScript, sementara aplikasi Flutter memakai ApiRepository dan MockRepository di balik antarmuka abstrak. Idenya agnostik bahasa; hanya alat perangkaiannya yang berbeda.

---

- HTML version: https://www.matthewswong.com/id/blog/mock-real-repository-adapter-offline-parity/
- English: https://www.matthewswong.com/en/blog/mock-real-repository-adapter-offline-parity/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
