Bangun Task Manager Sederhana dengan Kategori Menggunakan Node.js dan MongoDB
Pelajari cara mengelompokkan tugas berdasarkan kategori menggunakan Node.js dan MongoDB. Bangun REST API dengan endpoint create, list, get, update, delete serta filter kategori.
Daftar tugas sederhana biasanya cukup di awal: Anda menambahkan beberapa tugas, lalu menutupnya setelah selesai. Namun saat jumlah tugas terus bertambah, penggunaannya menjadi tidak nyaman. Salah satu cara memperbaikinya adalah menambahkan kategorisasi, sehingga setiap tugas dapat masuk ke kategori seperti Work, Study, Personal, dan sebagainya, lalu Anda bisa memfilter daftar sesuai kategorinya.
Dalam tutorial ini, Anda akan belajar cara membangun REST API yang memungkinkan Anda mengelompokkan tugas berdasarkan kategori menggunakan Node.js dan MongoDB. Di akhir, Anda akan memiliki server Node.js yang berfungsi dengan lima endpoint: create, list, get, update, dan delete, beserta filter kategori. Anda juga akan melihat cara menerapkan validasi kategori menggunakan driver MongoDB native, tanpa bergantung pada ODM seperti Mongoose.
Apa yang Akan Anda Pelajari
Cara membangun REST API dengan Node.js, Express, dan MongoDB, termasuk endpoint CRUD lengkap
Cara memvalidasi input, menerapkan kategori, dan menangani nilai ObjectId dengan aman secara efektif
Cara menstrukturkan dan menguji API Anda lalu menghubungkannya ke frontend
Anda dapat menemukan kode lengkap untuk tutorial ini di GitHub jika Anda ingin mengkloningnya dan mengikuti sambil membaca.
Prasyarat
Sebelum mulai, Anda sebaiknya sudah memiliki:
Node.js 18+ terpasang (npm sudah termasuk secara default)
MongoDB berjalan secara lokal, atau cluster MongoDB Atlas gratis
Postman atau klien HTTP apa pun
Pengetahuan dasar JavaScript dan konsep REST
Langkah 1: Siapkan Proyek
Mari kita lanjut membangun task manager sederhana. Pertama, buat folder baru dan inisialisasi proyek Node dengan menjalankan perintah di bawah ini di terminal:
mkdir task-manager-categories
cd task-manager-categories
npm init -yDidukung oleh
Berikutnya, kita akan memasang empat dependensi runtime yang dibutuhkan aplikasi untuk berjalan: express untuk routing, driver resmi MongoDB, dotenv untuk memuat variabel lingkungan, dan terakhir helmet untuk header keamanan.
npminstall express mongodb dotenv helmetDidukung oleh
Untuk pengembangan, pasang nodemon sebagai dev dependency dengan perintah di bawah agar server memuat ulang otomatis saat file berubah:
Selanjutnya, buka folder proyek di IDE Anda dan perbarui bagian scripts di package.json untuk menyertakan hal berikut. Ini akan memungkinkan Anda memulai server dengan npm start atau menjalankannya dalam mode pengembangan dengan npm run dev:
"scripts":{"start":"node server.js","dev":"nodemon server.js"}Didukung oleh
Berikut struktur yang akan kita bangun. Setiap folder punya peran jelas: db/ menangani koneksi database, lib/ berisi fungsi pembantu, middleware/ untuk middleware Express, dan routes/ mendefinisikan handler request. Folder public/ akan menampung frontend kecil yang akan kita tambahkan nanti. Bahkan untuk proyek kecil, struktur ini membuat alur mudah diikuti dan jelas ke mana kode baru harus ditempatkan. Silakan buat struktur folder sekarang dan isi sambil jalan, atau lewati ini dan tambahkan setiap file saat kita melangkah:
Untuk menyimpan dan mengambil tugas, pertama kita perlu menghubungkan aplikasi ke MongoDB. Ini dilakukan menggunakan connection string, yang memberi tahu driver MongoDB cara terhubung ke database Anda. Alih-alih menuliskannya langsung di kode, sebaiknya simpan di file environment. Ini menjaga nilai sensitif tetap di luar codebase dan memudahkan berpindah antar lingkungan.
Buat file .env.example untuk mendokumentasikan variabel yang diperlukan, dan file .env untuk nilai aktual Anda, seperti ini:
# Copy this file to `.env` and fill in real values.# --- Local MongoDB ---# Use this if you're running MongoDB locallyMONGO_URI=mongodb://127.0.0.1:27017
# --- MongoDB Atlas ---# Replace <username>, <password>, and <cluster-url> with your actual values# Example: mongodb+srv://user:[email protected]/# MONGO_URI=mongodb+srv://<username>:<password>@<cluster-url>/?retryWrites=true&w=majorityDB_NAME=taskmanager
PORT=3000Didukung oleh
Sekarang kita sudah punya connection string, kita bisa menggunakannya untuk menyiapkan koneksi MongoDB. Silakan buat file db/connect.js. Di sinilah kita menginisialisasi klien MongoDB dan membuatnya tersedia untuk bagian aplikasi lainnya:
const{ MongoClient }=require("mongodb");let client;let db;asyncfunctionconnectDB(){if(!process.env.MONGO_URI){thrownewError("MONGO_URI is not set. Check your .env file.");}
client =newMongoClient(process.env.MONGO_URI,{appName:"devrel-tutorial-javascript-crud-geeksforgeeks",});await client.connect();
db = client.db(process.env.DB_NAME||"taskmanager");await db.collection("tasks").createIndex({category:1});
console.log(`MongoDB connected (db: ${db.databaseName})`);}functiongetTasksCollection(){if(!db){thrownewError("Database not initialized. Call connectDB() first.");}return db.collection("tasks");}asyncfunctioncloseDB(){if(client)await client.close();}
module.exports ={ connectDB, getTasksCollection, closeDB };Didukung oleh
File ini menyiapkan satu klien MongoDB yang dapat digunakan ulang oleh seluruh aplikasi. Anda hanya ingin membuat ini sekali, karena driver sudah menangani connection pooling di balik layar. Membuat klien baru pada setiap request mungkin terlihat baik di awal, tetapi akan cepat menimbulkan masalah performa.
Langkah 3: Definisikan Struktur dan Validasi Task
Pada tahap ini, aplikasi bisa terhubung ke MongoDB. Sekarang kita perlu mendefinisikan seperti apa bentuk sebuah task sebelum mulai menyimpan apa pun.
Di sinilah penggunaan driver MongoDB native terasa sedikit berbeda. Tidak ada file skema seperti saat menggunakan Mongoose. Sebaliknya, "skema" hanyalah bentuk objek yang Anda sisipkan ke database. Ini mungkin terasa agak longgar di awal, tetapi sebenarnya membantu karena Anda tetap dekat dengan cara MongoDB benar-benar bekerja, dan tidak ada yang disembunyikan oleh abstraksi.
Kita tetap menginginkan validasi. Jadi, alih-alih menyebarkan logika itu di berbagai route, kita akan menyimpannya di satu tempat agar semuanya mengikuti aturan yang sama. Silakan buat file bernama lib/taskDocument.js dan tambahkan kode berikut:
classValidationErrorextendsError{constructor(message){super(message);this.name ='ValidationError';}}constALLOWED_CATEGORIES=['Work','Personal','Study','Other'];functionassertValidCategory(category){if(!ALLOWED_CATEGORIES.includes(category)){thrownewValidationError(`category must be one of: ${ALLOWED_CATEGORIES.join(', ')}`);}}functionbuildTaskDocument(body ={}){if(!body.title ||typeof body.title !=='string'||!body.title.trim()){thrownewValidationError('title is required and must be a non-empty string');}if(body.category !=null){assertValidCategory(body.category);}const now =newDate();return{title: body.title.trim(),description:typeof body.description ==='string'? body.description.trim():'',category: body.category !=null? body.category :'Other',completed:Boolean(body.completed),createdAt: now,updatedAt: now
};}functionbuildTaskUpdate(body ={}){const updates ={};if(typeof body.title ==='string'&& body.title.trim()){
updates.title = body.title.trim();}if(typeof body.description ==='string'){
updates.description = body.description.trim();}if(body.category !=null){assertValidCategory(body.category);
updates.category = body.category;}if(body.completed !=null){
updates.completed =Boolean(body.completed);}if(Object.keys(updates).length ===0){thrownewValidationError('no valid fields provided for update');}
updates.updatedAt =newDate();return updates;}
module.exports ={ALLOWED_CATEGORIES,
buildTaskDocument,
buildTaskUpdate,
ValidationError
};Didukung oleh
File ini bertindak sebagai gerbang untuk apa pun yang masuk ke database Anda. Setiap permintaan create atau update akan melewati sini, sehingga ada satu tempat yang menegakkan aturan.
Ini memastikan task selalu memiliki bentuk yang tepat, menjaga konsistensi kategori, dan menghindari kejutan saat Anda mulai melakukan query. ValidationError kustom juga memberi cara yang rapi untuk memisahkan input buruk dari masalah server sebenarnya, sehingga API Anda bisa merespons secara tepat. Dengan ini, bagian aplikasi lainnya bisa tetap sederhana. Setiap route bisa fokus pada tugasnya, dengan asumsi data yang diterima sudah valid. Selanjutnya, kita akan menyambungkan route dan mulai menyimpan task ke MongoDB.
Langkah 4: Tangani ObjectId dan Validasi Request
Sekarang kita sudah tahu seperti apa bentuk task, mari bahas cara mereferensikannya. Setiap kali sebuah route menyertakan parameter :id, nilai itu datang sebagai string biasa. Namun MongoDB mengharapkan sebuah ObjectId. Jika string itu tidak valid, driver akan melempar error yang kurang membantu. Alih-alih menangani itu di setiap route, kita akan memusatkannya dengan middleware agar setiap endpoint berperilaku sama.
Untuk melakukan ini, buat file bernama middleware/parseObjectId.js dan tambahkan berikut ini:
const{ ObjectId }=require('mongodb');functionparseObjectId(req, res, next){const{ id }= req.params;if(!ObjectId.isValid(id)){return res.status(400).json({error:'invalid task id'});}
req.taskId =newObjectId(id);next();}
module.exports = parseObjectId;Didukung oleh
Middleware ini berjalan sebelum handler route Anda. Ia memeriksa apakah id valid, mengonversinya menjadi ObjectId, dan menempelkannya ke req.taskId. Dengan begitu, saat logika route berjalan, Anda selalu bekerja dengan ObjectId yang benar, dan input buruk ditolak lebih awal dengan respons 400 yang jelas. Selanjutnya, kita akan memasangnya ke route dan mulai menyambungkan semuanya.
Langkah 5: Bangun Route Task (CRUD + Penyaringan)
Pada tahap ini, sebagian besar pekerjaan berat sudah selesai. Kita telah mendefinisikan seperti apa task yang valid, dan kita sudah menangani cara parsing dan validasi ID. Itu berarti handler route kita bisa tetap fokus pada satu hal: berbicara dengan database.
Mari kita bangun route API sebenarnya dengan membuat file bernama routes/tasks.js dan menambahkan berikut ini:
const express =require('express');const{ getTasksCollection }=require('../db/connect');const{ALLOWED_CATEGORIES,
buildTaskDocument,
buildTaskUpdate,
ValidationError
}=require('../lib/taskDocument');const parseObjectId =require('../middleware/parseObjectId');const router = express.Router();// POST /tasks
router.post('/',async(req, res, next)=>{try{const doc =buildTaskDocument(req.body);const result =awaitgetTasksCollection().insertOne(doc);
res.status(201).json({_id: result.insertedId,...doc });}catch(err){if(err instanceofValidationError){return res.status(400).json({error: err.message });}next(err);}});// GET /tasks (optionally ?category=Work)
router.get('/',async(req, res, next)=>{try{const{ category }= req.query;if(category &&!ALLOWED_CATEGORIES.includes(category)){return res.status(400).json({error:`category must be one of: ${ALLOWED_CATEGORIES.join(', ')}`});}const filter = category ?{ category }:{};const tasks =awaitgetTasksCollection().find(filter).sort({createdAt:-1}).toArray();
res.json(tasks);}catch(err){next(err);}});// GET /tasks/:id
router.get('/:id', parseObjectId,async(req, res, next)=>{try{const task =awaitgetTasksCollection().findOne({_id: req.taskId });if(!task)return res.status(404).json({error:'task not found'});
res.json(task);}catch(err){next(err);}});// PUT /tasks/:id
router.put('/:id', parseObjectId,async(req, res, next)=>{try{const updates =buildTaskUpdate(req.body);const result =awaitgetTasksCollection().findOneAndUpdate({_id: req.taskId },{$set: updates },{returnDocument:'after'});if(!result)return res.status(404).json({error:'task not found'});
res.json(result);}catch(err){if(err instanceofValidationError){return res.status(400).json({error: err.message });}next(err);}});// DELETE /tasks/:id
router.delete('/:id', parseObjectId,async(req, res, next)=>{try{const result =awaitgetTasksCollection().deleteOne({_id: req.taskId });if(result.deletedCount ===0){return res.status(404).json({error:'task not found'});}
res.status(204).end();}catch(err){next(err);}});
module.exports = router;Didukung oleh
Setiap route pada kode di atas mengikuti pola yang sama: mengambil input dari request, melewatkannya melalui helper yang kita buat sebelumnya, memanggil MongoDB, lalu mengembalikan respons. Dengan validasi dan parsing ID yang sudah ditangani, kode di sini tetap ringkas dan dapat diprediksi. Praktiknya seperti ini:
Route POST membangun task baru menggunakan buildTaskDocument sebelum menyisipkannya
Route GET secara opsional memfilter berdasarkan kategori dan mengembalikan hasil yang diurutkan dari yang terbaru
Route PUT menggunakan buildTaskUpdate, sehingga pembaruan parsial tetap aman dan konsisten
Route DELETE menghapus task berdasarkan ObjectId yang sudah diparse
Ada beberapa detail lain dalam kode ini yang juga patut dicatat:
find() mengembalikan cursor, bukan array, jadi kita rantai dengan .toArray() setelah sorting untuk benar-benar mendapatkan hasil
findOneAndUpdate dengan returnDocument: 'after' memberikan dokumen yang sudah diperbarui secara langsung
Kita mengembalikan kode status HTTP yang tepat: 201 untuk create, 204 untuk delete, dan 400 atau 404 sesuai kebutuhan
Error tak terduga diteruskan ke next(err), sehingga bisa ditangani di satu tempat alih-alih dalam setiap route
Langkah 6: Sambungkan Semuanya di Server
Pada titik ini, semua bagian sudah siap. Kita punya validasi, route yang bersih, dan koneksi database yang berfungsi. Sekarang kita hanya perlu menyambungkan semuanya dan benar-benar menjalankan server. Buat file server.js. Ini adalah entry point aplikasi, tempat semuanya dipadukan:
require('dotenv').config();const path =require('path');const express =require('express');const helmet =require('helmet');const{ connectDB, closeDB }=require('./db/connect');const tasksRouter =require('./routes/tasks');if(!process.env.MONGO_URI){
console.error('Fatal: MONGO_URI is not set. Copy .env.example to .env and fill it in.');
process.exit(1);}constPORT=Number(process.env.PORT)||3000;const app =express();
app.use(helmet());
app.use(express.json({limit:'100kb'}));
app.use(express.static(path.join(__dirname,'public')));
app.get('/health',(req, res)=>{
res.json({status:'ok',service:'task-manager-with-categories'});});
app.use('/tasks', tasksRouter);
app.use((req, res)=>{
res.status(404).json({error:'not found'});});
app.use((err, req, res, next)=>{if(err.type ==='entity.too.large'){return res.status(413).json({error:'payload too large'});}if(err.type ==='entity.parse.failed'){return res.status(400).json({error:'invalid JSON body'});}
console.error(err);
res.status(500).json({error:'internal server error'});});asyncfunctionstart(){try{awaitconnectDB();const server = app.listen(PORT,()=>{
console.log(`Server running on http://localhost:${PORT}`);});constshutdown=()=>{
console.log('\nShutting down gracefully...');
server.close(async()=>{awaitcloseDB();
process.exit(0);});setTimeout(()=> process.exit(1),10_000).unref();};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);}catch(err){
console.error('Failed to start server:', err);
process.exit(1);}}if(require.main === module){start();}
module.exports ={ app, start };Didukung oleh
File ini merangkai semuanya dengan rapi. Ia memeriksa variabel lingkungan sebelum apa pun berjalan, menerapkan keamanan dasar dengan helmet, dan membatasi JSON yang masuk agar aplikasi Anda tidak menerima payload sangat besar secara tidak sengaja. Ia juga menyajikan folder public/, sehingga frontend Anda bisa berada dalam aplikasi yang sama tanpa memerlukan server terpisah.
Semua route Anda dipasang di bawah /tasks, dan apa pun yang tidak cocok akan mengembalikan 404 yang bersih. Error ditangani di satu tempat alih-alih diulang di setiap route, sehingga tetap konsisten dan mudah dirawat. Koneksi database dibuat sebelum server mulai mendengarkan, sehingga Anda tidak pernah menerima request sebelum siap, dan proses penghentian ditangani dengan baik, sehingga koneksi ditutup dengan benar.
Sekarang jalankan dan pastikan semuanya bekerja dengan menjalankan npm run dev. Jika semuanya tersambung dengan benar, Anda akan melihat seperti ini:
MongoDB connected (db: taskmanager)
Server running on http://localhost:3000
Didukung oleh
Pada titik ini, semuanya sudah tersambung dan berjalan! Kita kini memiliki API yang sepenuhnya berfungsi, jadi mari kita uji.
Langkah 7: Uji Endpoint API
Sebelum membangun UI, ada baiknya memastikan setiap endpoint berfungsi sendiri. Ini membuat debugging jauh lebih mudah, karena Anda dapat cepat mengetahui apakah masalah berasal dari API atau frontend. Kita akan menggunakan Postman (atau klien HTTP apa pun) dan menjalankan beberapa permintaan secara berurutan. Masing-masing bergantung pada data yang dibuat pada langkah sebelumnya.
Buat sebuah task: Kirim request POST dengan title, description, dan category. Jika berhasil, API mengembalikan 201 Created beserta task baru dan _id yang dihasilkan.
POST http://localhost:3000/tasks
Content-Type: application/json
{"title":"Write GeeksForGeeks article",
"description":"First draft by Friday",
"category":"Work"}Didukung oleh
Buat beberapa lagi: Jalankan request POST yang sama dengan body berbeda, sehingga Anda punya cukup data untuk menguji listing dan filtering. Setelah ini, Anda akan memiliki dataset kecil untuk di-query.
{"title":"Go for a run","category":"Personal"}{"title":"Read MongoDB docs","category":"Study"}{"title":"Buy groceries"}// category defaults to "Other"Didukung oleh
Daftar semua task: Ini mengembalikan semua task, diurutkan dari yang terbaru.
Filter berdasarkan kategori: Ini hanya mengembalikan task kategori Work. Indeks yang Anda tambahkan sebelumnya menjaga query ini tetap cepat saat data bertambah.
GET http://localhost:3000/tasks?category=WorkDidukung oleh
Perbarui sebuah task: Gunakan _id dari salah satu task yang baru Anda buat (bisa disalin dari respons POST atau endpoint list). Ini mengembalikan task yang sudah diperbarui. Anda hanya perlu mengirim field yang ingin diubah, sehingga pembaruan parsial tetap sederhana.
PUT http://localhost:3000/tasks/<paste-task-id-here>
Content-Type: application/json
{"completed":true}Didukung oleh
Hapus sebuah task: Gunakan _id dari task yang ingin dihapus. Ini mengembalikan 204 No Content. Jika Anda mencoba mengambil task yang sama lagi, Anda akan mendapatkan 404 dengan "task not found".
DELETE http://localhost:3000/tasks/<paste-task-id-here>Didukung oleh
Pada titik ini, Anda telah memastikan semua route berperilaku sesuai harapan. API sudah menjalankan tugasnya, jadi sekarang kita bisa lanjut membangun UI di atasnya.
Langkah 8: Tambahkan Frontend untuk Berinteraksi dengan API
Postman bagus untuk memverifikasi API, tetapi akan lebih baik melihat cara kerjanya di situs web nyata, jadi mari tambahkan sedikit kode frontend ke aplikasi kita.
Alih-alih menempelkan file HTML dan CSS besar di sini, saya sudah menambahkan kodenya ke repositori GitHub ini. Buka ke sana dan salin isi dari index.html, styles.css, dan favicon.svg ke folder public/ di root proyek Anda. Simpan perubahan lalu buka localhost:3000. Kemudian tempelkan kode di bawah ini ke file app.js:
const form = document.getElementById('task-form');const titleEl = document.getElementById('title');const descEl = document.getElementById('description');const catEl = document.getElementById('category');const errEl = document.getElementById('form-error');const listEl = document.getElementById('task-list');const emptyEl = document.getElementById('empty');const filterEl = document.getElementById('filter');const countEl = document.getElementById('task-count');asyncfunctionapi(method, path, body){const res =awaitfetch(path,{
method,headers: body ?{'Content-Type':'application/json'}:{},body: body ?JSON.stringify(body):undefined});if(res.status ===204)returnnull;const data =await res.json();if(!res.ok)thrownewError(data.error ||`Request failed (${res.status})`);return data;}constfetchTasks=(category)=>api('GET','/tasks'+(category ?`?category=${encodeURIComponent(category)}`:''));constcreateTask=(body)=>api('POST','/tasks', body);constupdateTask=(id, body)=>api('PUT',`/tasks/${id}`, body);constdeleteTask=(id)=>api('DELETE',`/tasks/${id}`);functionupdateCount(tasks){const active = tasks.filter((t)=>!t.completed).length;const total = tasks.length;if(total ===0){
countEl.textContent ='';return;}const scope = filterEl.value ?` in ${filterEl.value}`:'';
countEl.textContent =`${total} tasks${scope} · ${active} active`;}functionrenderTask(task){const li = document.createElement('li');
li.className ='task'+(task.completed ?' completed':'');const checkbox = document.createElement('input');
checkbox.type ='checkbox';
checkbox.checked = task.completed;
checkbox.addEventListener('change',()=>updateTask(task._id,{completed: checkbox.checked }).then(refresh));const title = document.createElement('div');
title.textContent = task.title;const badge = document.createElement('span');
badge.textContent = task.category;const del = document.createElement('button');
del.textContent ='Delete';
del.onclick=()=>deleteTask(task._id).then(refresh);
li.append(checkbox, title, badge, del);return li;}functionrender(tasks){
listEl.innerHTML ='';
emptyEl.classList.toggle('hidden', tasks.length >0);for(const task of tasks){
listEl.appendChild(renderTask(task));}updateCount(tasks);}asyncfunctionrefresh(){try{const tasks =awaitfetchTasks(filterEl.value);render(tasks);}catch(e){
errEl.textContent = e.message;}}
form.addEventListener('submit',async(e)=>{
e.preventDefault();
errEl.textContent ='';const title = titleEl.value.trim();if(!title){
errEl.textContent ='Title is required.';return;}try{awaitcreateTask({
title,description: descEl.value.trim(),category: catEl.value
});
form.reset();awaitrefresh();}catch(e){
errEl.textContent = e.message;}});
filterEl.addEventListener('change', refresh);refresh();Didukung oleh
Kode JavaScript ini adalah tempat semuanya terhubung kembali ke API yang Anda bangun sebelumnya. Setiap fungsi dipetakan ke salah satu route Anda, sehingga frontend mudah diikuti. Semua request juga melalui satu helper api(), sehingga Anda hanya menangani error di satu tempat alih-alih mengulang logika yang sama di mana-mana.
Penyaringan bekerja dengan mengirim query string seperti ?category=Work, yang terhubung langsung ke logika backend yang sudah Anda tambahkan. Setelah membuat, memperbarui, atau menghapus task, aplikasi mengambil daftar terbaru lagi agar UI tetap sinkron. Penghitung task memang detail kecil, tetapi membuat aplikasi terasa lebih hidup saat digunakan.
Sekarang mulai ulang server Anda dengan npm run dev, lalu buka http://localhost:3000 untuk mencoba Task Manager. Tambahkan beberapa task, perbarui, beralih antar kategori, dan hapus juga. Pada tahap ini, frontend dan backend sudah sepenuhnya terhubung dan bekerja dari ujung ke ujung.
Ringkasan
Selamat! Anda berhasil membangun API task manager lengkap dari nol menggunakan Node.js, Express, dan MongoDB. Anda menangani validasi, menjaga route tetap ringkas, dan menyambungkan semuanya menjadi sistem yang berfungsi. Lebih penting lagi, Anda tetap dekat dengan cara MongoDB sebenarnya bekerja alih-alih menyembunyikannya di balik abstraksi. Dari sini, Anda bisa mulai mengembangkan aplikasi agar siap produksi.
Hal-Hal Penting
Menggunakan middleware dan memusatkan validasi menjaga kode Anda tetap bersih dan dapat diprediksi.
Driver MongoDB native memberi Anda kendali tanpa abstraksi yang tidak perlu.
API yang terstruktur dengan baik mudah diperluas menjadi aplikasi siap produksi.
FAQs
Apakah Saya Perlu Mongoose untuk Membangun API MongoDB?⌄
Tidak. Tutorial ini menggunakan driver MongoDB native, yang memberi Anda lebih banyak kontrol dan tetap ringan. Anda dapat menambahkan Mongoose nanti jika menginginkan abstraksi skema.
Bagaimana Saya Memvalidasi Data Tanpa Skema?⌄
Anda dapat memusatkan validasi dalam fungsi helper (seperti buildTaskDocument), memastikan setiap route menegakkan aturan yang sama sebelum menulis ke database.
Apa yang Terjadi Jika Saya Mengirim ObjectId Tidak Valid?⌄
Middleware akan menolaknya lebih awal dengan respons 400, mencegah MongoDB melempar error yang membingungkan.
Kuasai pertanyaan wawancara basis data, dari konsep SQL dasar hingga skenario desain sistem tingkat lanjut. Panduan mendalam ini mencakup semua yang Anda perlukan untuk sukses di wawancara DBMS dan meraih peran berikutnya.
Temukan alasan mengapa Anda sebaiknya (tidak) menggunakan spaghetti plot untuk menyampaikan ketidakpastian jalur prediksi badai serta dampaknya terhadap interpretasi.
Dalam kursus konseptual ini (tidak memerlukan pemrograman), Anda akan mempelajari empat jenis utama basis data NoSQL dan mesin-mesin populer yang digunakan.
Daftar tugas sederhana biasanya cukup di awal: Anda menambahkan beberapa tugas, lalu menutupnya setelah selesai. Namun saat jumlah tugas terus bertambah, penggunaannya menjadi tidak nyaman. Salah satu cara memperbaikinya adalah menambahkan kategorisasi, sehingga setiap tugas dapat masuk ke kategori seperti Work, Study, Personal, dan sebagainya, lalu Anda bisa memfilter daftar sesuai kategorinya.
Dalam tutorial ini, Anda akan belajar cara membangun REST API yang memungkinkan Anda mengelompokkan tugas berdasarkan kategori menggunakan Node.js dan MongoDB. Di akhir, Anda akan memiliki server Node.js yang berfungsi dengan lima endpoint: create, list, get, update, dan delete, beserta filter kategori. Anda juga akan melihat cara menerapkan validasi kategori menggunakan driver MongoDB native, tanpa bergantung pada ODM seperti Mongoose.
Apa yang Akan Anda Pelajari
Anda dapat menemukan kode lengkap untuk tutorial ini di GitHub jika Anda ingin mengkloningnya dan mengikuti sambil membaca.
Prasyarat
Sebelum mulai, Anda sebaiknya sudah memiliki:
Langkah 1: Siapkan Proyek
Mari kita lanjut membangun task manager sederhana. Pertama, buat folder baru dan inisialisasi proyek Node dengan menjalankan perintah di bawah ini di terminal:
Berikutnya, kita akan memasang empat dependensi runtime yang dibutuhkan aplikasi untuk berjalan: express untuk routing, driver resmi MongoDB, dotenv untuk memuat variabel lingkungan, dan terakhir helmet untuk header keamanan.
Untuk pengembangan, pasang nodemon sebagai dev dependency dengan perintah di bawah agar server memuat ulang otomatis saat file berubah:
Selanjutnya, buka folder proyek di IDE Anda dan perbarui bagian scripts di package.json untuk menyertakan hal berikut. Ini akan memungkinkan Anda memulai server dengan npm start atau menjalankannya dalam mode pengembangan dengan npm run dev:
Berikut struktur yang akan kita bangun. Setiap folder punya peran jelas: db/ menangani koneksi database, lib/ berisi fungsi pembantu, middleware/ untuk middleware Express, dan routes/ mendefinisikan handler request. Folder public/ akan menampung frontend kecil yang akan kita tambahkan nanti. Bahkan untuk proyek kecil, struktur ini membuat alur mudah diikuti dan jelas ke mana kode baru harus ditempatkan. Silakan buat struktur folder sekarang dan isi sambil jalan, atau lewati ini dan tambahkan setiap file saat kita melangkah:
Langkah 2: Sambungkan ke MongoDB
Untuk menyimpan dan mengambil tugas, pertama kita perlu menghubungkan aplikasi ke MongoDB. Ini dilakukan menggunakan connection string, yang memberi tahu driver MongoDB cara terhubung ke database Anda. Alih-alih menuliskannya langsung di kode, sebaiknya simpan di file environment. Ini menjaga nilai sensitif tetap di luar codebase dan memudahkan berpindah antar lingkungan.
Buat file .env.example untuk mendokumentasikan variabel yang diperlukan, dan file .env untuk nilai aktual Anda, seperti ini:
Sekarang kita sudah punya connection string, kita bisa menggunakannya untuk menyiapkan koneksi MongoDB. Silakan buat file db/connect.js. Di sinilah kita menginisialisasi klien MongoDB dan membuatnya tersedia untuk bagian aplikasi lainnya:
File ini menyiapkan satu klien MongoDB yang dapat digunakan ulang oleh seluruh aplikasi. Anda hanya ingin membuat ini sekali, karena driver sudah menangani connection pooling di balik layar. Membuat klien baru pada setiap request mungkin terlihat baik di awal, tetapi akan cepat menimbulkan masalah performa.
Langkah 3: Definisikan Struktur dan Validasi Task
Pada tahap ini, aplikasi bisa terhubung ke MongoDB. Sekarang kita perlu mendefinisikan seperti apa bentuk sebuah task sebelum mulai menyimpan apa pun.
Di sinilah penggunaan driver MongoDB native terasa sedikit berbeda. Tidak ada file skema seperti saat menggunakan Mongoose. Sebaliknya, "skema" hanyalah bentuk objek yang Anda sisipkan ke database. Ini mungkin terasa agak longgar di awal, tetapi sebenarnya membantu karena Anda tetap dekat dengan cara MongoDB benar-benar bekerja, dan tidak ada yang disembunyikan oleh abstraksi.
Kita tetap menginginkan validasi. Jadi, alih-alih menyebarkan logika itu di berbagai route, kita akan menyimpannya di satu tempat agar semuanya mengikuti aturan yang sama. Silakan buat file bernama lib/taskDocument.js dan tambahkan kode berikut:
File ini bertindak sebagai gerbang untuk apa pun yang masuk ke database Anda. Setiap permintaan create atau update akan melewati sini, sehingga ada satu tempat yang menegakkan aturan.
Ini memastikan task selalu memiliki bentuk yang tepat, menjaga konsistensi kategori, dan menghindari kejutan saat Anda mulai melakukan query. ValidationError kustom juga memberi cara yang rapi untuk memisahkan input buruk dari masalah server sebenarnya, sehingga API Anda bisa merespons secara tepat. Dengan ini, bagian aplikasi lainnya bisa tetap sederhana. Setiap route bisa fokus pada tugasnya, dengan asumsi data yang diterima sudah valid. Selanjutnya, kita akan menyambungkan route dan mulai menyimpan task ke MongoDB.
Langkah 4: Tangani ObjectId dan Validasi Request
Sekarang kita sudah tahu seperti apa bentuk task, mari bahas cara mereferensikannya. Setiap kali sebuah route menyertakan parameter :id, nilai itu datang sebagai string biasa. Namun MongoDB mengharapkan sebuah ObjectId. Jika string itu tidak valid, driver akan melempar error yang kurang membantu. Alih-alih menangani itu di setiap route, kita akan memusatkannya dengan middleware agar setiap endpoint berperilaku sama.
Untuk melakukan ini, buat file bernama middleware/parseObjectId.js dan tambahkan berikut ini:
Middleware ini berjalan sebelum handler route Anda. Ia memeriksa apakah id valid, mengonversinya menjadi ObjectId, dan menempelkannya ke req.taskId. Dengan begitu, saat logika route berjalan, Anda selalu bekerja dengan ObjectId yang benar, dan input buruk ditolak lebih awal dengan respons 400 yang jelas. Selanjutnya, kita akan memasangnya ke route dan mulai menyambungkan semuanya.
Langkah 5: Bangun Route Task (CRUD + Penyaringan)
Pada tahap ini, sebagian besar pekerjaan berat sudah selesai. Kita telah mendefinisikan seperti apa task yang valid, dan kita sudah menangani cara parsing dan validasi ID. Itu berarti handler route kita bisa tetap fokus pada satu hal: berbicara dengan database.
Mari kita bangun route API sebenarnya dengan membuat file bernama routes/tasks.js dan menambahkan berikut ini:
Setiap route pada kode di atas mengikuti pola yang sama: mengambil input dari request, melewatkannya melalui helper yang kita buat sebelumnya, memanggil MongoDB, lalu mengembalikan respons. Dengan validasi dan parsing ID yang sudah ditangani, kode di sini tetap ringkas dan dapat diprediksi. Praktiknya seperti ini:
Ada beberapa detail lain dalam kode ini yang juga patut dicatat:
Langkah 6: Sambungkan Semuanya di Server
Pada titik ini, semua bagian sudah siap. Kita punya validasi, route yang bersih, dan koneksi database yang berfungsi. Sekarang kita hanya perlu menyambungkan semuanya dan benar-benar menjalankan server. Buat file server.js. Ini adalah entry point aplikasi, tempat semuanya dipadukan:
File ini merangkai semuanya dengan rapi. Ia memeriksa variabel lingkungan sebelum apa pun berjalan, menerapkan keamanan dasar dengan helmet, dan membatasi JSON yang masuk agar aplikasi Anda tidak menerima payload sangat besar secara tidak sengaja. Ia juga menyajikan folder public/, sehingga frontend Anda bisa berada dalam aplikasi yang sama tanpa memerlukan server terpisah.
Semua route Anda dipasang di bawah /tasks, dan apa pun yang tidak cocok akan mengembalikan 404 yang bersih. Error ditangani di satu tempat alih-alih diulang di setiap route, sehingga tetap konsisten dan mudah dirawat. Koneksi database dibuat sebelum server mulai mendengarkan, sehingga Anda tidak pernah menerima request sebelum siap, dan proses penghentian ditangani dengan baik, sehingga koneksi ditutup dengan benar.
Sekarang jalankan dan pastikan semuanya bekerja dengan menjalankan npm run dev. Jika semuanya tersambung dengan benar, Anda akan melihat seperti ini:
Pada titik ini, semuanya sudah tersambung dan berjalan! Kita kini memiliki API yang sepenuhnya berfungsi, jadi mari kita uji.
Langkah 7: Uji Endpoint API
Sebelum membangun UI, ada baiknya memastikan setiap endpoint berfungsi sendiri. Ini membuat debugging jauh lebih mudah, karena Anda dapat cepat mengetahui apakah masalah berasal dari API atau frontend. Kita akan menggunakan Postman (atau klien HTTP apa pun) dan menjalankan beberapa permintaan secara berurutan. Masing-masing bergantung pada data yang dibuat pada langkah sebelumnya.
Buat sebuah task: Kirim request POST dengan title, description, dan category. Jika berhasil, API mengembalikan 201 Created beserta task baru dan _id yang dihasilkan.
Buat beberapa lagi: Jalankan request POST yang sama dengan body berbeda, sehingga Anda punya cukup data untuk menguji listing dan filtering. Setelah ini, Anda akan memiliki dataset kecil untuk di-query.
Daftar semua task: Ini mengembalikan semua task, diurutkan dari yang terbaru.
Filter berdasarkan kategori: Ini hanya mengembalikan task kategori Work. Indeks yang Anda tambahkan sebelumnya menjaga query ini tetap cepat saat data bertambah.
Perbarui sebuah task: Gunakan _id dari salah satu task yang baru Anda buat (bisa disalin dari respons POST atau endpoint list). Ini mengembalikan task yang sudah diperbarui. Anda hanya perlu mengirim field yang ingin diubah, sehingga pembaruan parsial tetap sederhana.
Hapus sebuah task: Gunakan _id dari task yang ingin dihapus. Ini mengembalikan 204 No Content. Jika Anda mencoba mengambil task yang sama lagi, Anda akan mendapatkan 404 dengan "task not found".
Pada titik ini, Anda telah memastikan semua route berperilaku sesuai harapan. API sudah menjalankan tugasnya, jadi sekarang kita bisa lanjut membangun UI di atasnya.
Langkah 8: Tambahkan Frontend untuk Berinteraksi dengan API
Postman bagus untuk memverifikasi API, tetapi akan lebih baik melihat cara kerjanya di situs web nyata, jadi mari tambahkan sedikit kode frontend ke aplikasi kita.
Alih-alih menempelkan file HTML dan CSS besar di sini, saya sudah menambahkan kodenya ke repositori GitHub ini. Buka ke sana dan salin isi dari index.html, styles.css, dan favicon.svg ke folder public/ di root proyek Anda. Simpan perubahan lalu buka localhost:3000. Kemudian tempelkan kode di bawah ini ke file app.js:
Kode JavaScript ini adalah tempat semuanya terhubung kembali ke API yang Anda bangun sebelumnya. Setiap fungsi dipetakan ke salah satu route Anda, sehingga frontend mudah diikuti. Semua request juga melalui satu helper api(), sehingga Anda hanya menangani error di satu tempat alih-alih mengulang logika yang sama di mana-mana.
Penyaringan bekerja dengan mengirim query string seperti ?category=Work, yang terhubung langsung ke logika backend yang sudah Anda tambahkan. Setelah membuat, memperbarui, atau menghapus task, aplikasi mengambil daftar terbaru lagi agar UI tetap sinkron. Penghitung task memang detail kecil, tetapi membuat aplikasi terasa lebih hidup saat digunakan.
Sekarang mulai ulang server Anda dengan npm run dev, lalu buka http://localhost:3000 untuk mencoba Task Manager. Tambahkan beberapa task, perbarui, beralih antar kategori, dan hapus juga. Pada tahap ini, frontend dan backend sudah sepenuhnya terhubung dan bekerja dari ujung ke ujung.
Ringkasan
Selamat! Anda berhasil membangun API task manager lengkap dari nol menggunakan Node.js, Express, dan MongoDB. Anda menangani validasi, menjaga route tetap ringkas, dan menyambungkan semuanya menjadi sistem yang berfungsi. Lebih penting lagi, Anda tetap dekat dengan cara MongoDB sebenarnya bekerja alih-alih menyembunyikannya di balik abstraksi. Dari sini, Anda bisa mulai mengembangkan aplikasi agar siap produksi.
Hal-Hal Penting