Kalkulator pada proyek sebelumnya hidup dalam lingkungan yang sepenuhnya di bawah kendali program: setiap angka berasal dari ketukan tombol, dan perhitungan selesai dalam sekejap. Aplikasi cuaca menuntut kemampuan yang berbeda — suhu dan kelembapan Bandung tidak bisa ditebak oleh program; data itu harus diminta dari server. Begitu berbicara dengan server, program harus rela menunggu, menerima kemungkinan jawaban tidak pernah datang, dan tetap tampil sopan di hadapan pengguna selama menunggu. Keempat kewajiban itu dipecah menjadi empat bagian proyek.
Sama seperti kalkulator, pendekatan utama proyek ini adalah pemisahan logika dari tampilan. Seluruh logika — meminta data, memformatnya, mengelola keadaan, hingga mengoptimalkan permintaan — hidup di src/cuaca.mjs, sebuah module tanpa satu pun kode browser, sehingga semuanya bisa diuji langsung dengan node --test. Berkas HTML hanya bertugas menampilkan keadaan dan meneruskan input pengguna.
Satu keputusan penting sejak awal: course ini tidak memanggil API internet mana pun. Seluruh contoh fetch berbicara dengan server tiruan lokal buatan node:http yang hidup di dalam berkas demo — server tiruan menyajikan data JSON di localhost port 18080, lalu dimatikan lagi dengan server.close() di akhir demo. Pola ini membuat setiap contoh berjalan offline dengan keluaran yang selalu sama. Di bagian lain, nilai yang bergantung waktu (seperti umur entri cache) disuntikkan dalam bentuk fungsi — pola dependency injection yang dibahas pada section Architecture dan Patterns — sehingga uji tidak perlu benar-benar menunggu.
Proyek dikerjakan dalam empat bagian berurutan:
- Bagian 1: Mengambil Data API — menulis fungsi
muatCuaca(kota)denganfetch, memeriksaresponse.ok, melempar galat khususApiError, dan membuktikannya dengan server tiruan yang hidup di dalam berkas demo. Kita belajar bahwafetchhanya gagal saat jaringan gagal, sehingga kode status HTTP wajib kita periksa sendiri. - Bagian 2: Menampilkan Data Cuaca — mengubah data mentah (
24.6) menjadi tampilan yang enak dibaca (24,6°C) lewat fungsi murniformatCuacadanIntl.NumberFormat, lalu merendernya sebagai kartu di antarmuka. Kita belajar menjaga logika format tetap murni agar mudah diuji. - Bagian 3: Loading dan Error States — memodelkan antarmuka sebagai state machine sederhana dengan empat keadaan (
kosong,memuat,sukses,galat) dan satu pintu perubahan bernamakeadaanBerikut, dilengkapi pesan galat yang ramah. Kita belajar memastikan pengguna tidak pernah menatap layar kosong tanpa penjelasan. - Bagian 4: Optimasi Permintaan — menghemat permintaan dengan cache berbasis
Mapyang punya TTL, menunda eksekusi dengandebounce, dan membatalkan permintaan usang denganAbortController. Kita belajar bahwa aplikasi yang sopan kepada server juga terasa lebih responsif bagi penggunanya.
Struktur Berkas Proyek #
Seluruh kode proyek tinggal di tiga folder pada content/javascript/proyek/aplikasi-cuaca/:
aplikasi-cuaca/
├── src/
│ ├── cuaca.mjs # logika inti: fetch, formatter, state machine, cache
│ └── index.html.txt # contoh antarmuka (salin ulang sebagai index.html)
├── styles/
│ └── styles.css # tampilan panel pencarian dan kartu cuaca
└── tests/
└── cuaca.test.mjs # uji otomatis untuk cuaca.mjsPeran tiap berkas:
| Berkas | Peran |
|---|---|
src/cuaca.mjs | Otak aplikasi: muatCuaca, ApiError, formatter Intl, state machine, cache, dan debounce. Tidak menyentuh DOM. |
src/index.html.txt | Wajah aplikasi: form pencarian kota dan kartu cuaca yang dirender sesuai keadaan. |
styles/styles.css | Penampil: panel, input, tombol, serta warna status memuat dan galat. |
tests/cuaca.test.mjs | Penjaga: dua puluh uji otomatis yang memastikan seluruh perilaku logika, termasuk jalur fetch ke server tiruan. |
Setiap bagian menampilkan kondisi berkas pada akhir bagian tersebut: blok kode yang membahas sebuah berkas menyediakan tautan unduhan salinannya di header blok. Versi lengkap dan terakhir dari seluruh berkas selalu ada di folder src/, styles/, dan tests/ di atas — jika Anda mengikuti proyek dari Bagian 1, berkas-berkas itu tumbuh bertahap mengikuti pembahasan.
Menjalankan Pengujian #
Uji otomatis dijalankan dari folder proyek dengan test runner bawaan Node.js:
cd aplikasi-cuaca
node --testNode mencari berkas uji dengan pola nama seperti *.test.mjs, menemukan tests/cuaca.test.mjs, lalu menjalankan seluruh ujinya — termasuk uji yang benar-benar memanggil fetch ke server tiruan di localhost. Ringkasan di ujung keluarannya pada akhir proyek berbunyi:
ℹ tests 20
ℹ suites 7
ℹ pass 20
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 630.425184Angka pass 20 berarti seluruh uji lulus; duration_ms akan berbeda-beda di mesin Anda karena mengukur waktu eksekusi. Mulailah dari Bagian 1 dan ikuti urutannya — pada bagian akhir, aplikasi cuaca kita berjalan utuh: datanya diambil, ditampilkan dengan rapi, gagal dengan sopan, dan tidak pernah meminta hal yang sama dua kali.
Meminta data cuaca dari API dengan fetch dan menangani responsnya.
Buka → # Bagian 2: Menampilkan Data CuacaMerender data cuaca ke tampilan dengan format yang mudah dibaca.
Buka → # Bagian 3: Loading dan Error StatesMenampilkan keadaan memuat dan galat yang informatif bagi pengguna.
Buka → # Bagian 4: Optimasi PermintaanMenghemat permintaan dengan cache, debounce, dan AbortController.
Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
styles/styles.css
Unduh berkas/* styles.css — tampilan aplikasi cuaca */
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #e0f2fe;
font-family: system-ui, sans-serif;
}
.panel {
width: 300px;
padding: 20px;
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.panel h1 {
margin: 0 0 12px;
font-size: 1.25rem;
color: #0c4a6e;
}
#formulir-cari {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
#kolom-kota {
flex: 1;
padding: 8px 10px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 1rem;
}
#formulir-cari button {
padding: 8px 14px;
border: none;
border-radius: 8px;
background-color: #0284c7;
color: #ffffff;
font-size: 1rem;
cursor: pointer;
}
#formulir-cari button:hover {
background-color: #0369a1;
}
.kartu {
min-height: 80px;
padding: 12px;
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.95rem;
color: #334155;
white-space: pre-wrap;
}
.kartu h2 {
margin: 0 0 8px;
font-size: 1.1rem;
color: #0c4a6e;
}
.kartu ul {
margin: 0;
padding-left: 18px;
}
.kartu li {
margin-bottom: 4px;
}
.status--memuat {
color: #b45309;
}
.status--galat {
color: #b91c1c;
font-weight: 600;
}