Cara Restrukturisasi CRUD Node.js: Dari Function ke Folder Controller-Service-Model
Admin
Penulis Artikel
Cara Restrukturisasi CRUD Node.js: Dari Function ke Folder Controller-Service-Model
Kalau kamu baru belajar backend dengan Node.js dan MongoDB, kemungkinan besar kode CRUD pertamamu terlihat seperti ini: semua fungsi create, read, update, delete ditulis di satu file, tanpa endpoint HTTP, tanpa struktur folder. Ini wajar — cara ini bagus untuk memahami logika dasar. Tapi begitu proyekmu mulai punya banyak fitur, kode seperti ini akan cepat berantakan dan sulit dikembangkan.
Artikel ini akan membahas cara merestrukturisasi kode CRUD dari kumpulan function menjadi struktur folder yang rapi menggunakan pola Controller-Service-Model, lengkap dengan contoh implementasi nyata: membangun Todo List API.
Artikel ini adalah bagian pertama dari seri “Backend Node.js untuk Pemula”. Setelah ini, folder yang kita bangun akan terus dikembangkan di artikel selanjutnya: Middleware dan .env di Express.js hingga Struktur Project Node.js Siap Produksi.
1. Kenapa Kode Function-Based Perlu Direstrukturisasi?
Bayangkan kode awal seperti ini (tanpa Express, tanpa endpoint — murni function untuk baca/tulis data):
// todo.js — versi awal, semua jadi satu
const todos = [];
function createTodo(title) {
const todo = { id: Date.now(), title, completed: false };
todos.push(todo);
return todo;
}
function getAllTodos() {
return todos;
}
function getTodoById(id) {
return todos.find((t) => t.id === id);
}
function updateTodo(id, data) {
const todo = getTodoById(id);
if (!todo) return null;
Object.assign(todo, data);
return todo;
}
function deleteTodo(id) {
const index = todos.findIndex((t) => t.id === id);
if (index === -1) return false;
todos.splice(index, 1);
return true;
}
module.exports = { createTodo, getAllTodos, getTodoById, updateTodo, deleteTodo };
Kode ini berfungsi, tapi punya beberapa masalah begitu proyek berkembang:
Tidak ada endpoint HTTP — belum bisa diakses lewat browser, Postman, atau frontend.
Data disimpan di memori — hilang setiap server restart.
Semua logika bercampur — jika nanti ditambah validasi, koneksi database, dan response HTTP, satu file ini akan membengkak jadi ratusan baris.
Susah di-testing — logika bisnis (business logic) menyatu dengan logika penyimpanan data.
Susah dikembangkan tim — kalau dua orang mengerjakan fitur berbeda di file yang sama, akan sering terjadi conflict.
Solusinya adalah memisahkan tanggung jawab (separation of concerns) ke dalam beberapa lapisan yang punya tugas jelas.
2. Konsep Controller-Service-Model
Pola ini membagi kode backend menjadi tiga lapisan utama:
Lapisan | Tugas | Contoh |
|---|---|---|
Router | Menentukan endpoint dan method HTTP mana yang memanggil controller mana |
|
Controller | Menangani request & response HTTP (menerima | Ambil |
Service | Berisi business logic murni, tidak tahu soal HTTP | Validasi aturan bisnis, olah data sebelum/sesudah ke database |
Model | Mendefinisikan struktur data dan cara berkomunikasi dengan database | Schema Mongoose untuk collection |
Alur request-nya seperti ini:
Client (Postman/Browser)
│
▼
Router → menentukan endpoint mana yang dipanggil
│
▼
Controller → ambil data dari request, panggil service
│
▼
Service → jalankan business logic, panggil model
│
▼
Model → query ke MongoDB
│
▼
Response dikirim balik ke Client
Keuntungan pola ini:
Mudah dilacak — kalau ada bug di query database, kamu tahu harus cek model. Kalau bug di validasi, cek service.
Reusable — service bisa dipanggil dari controller lain, atau dari script terpisah (misal seeding data), tanpa bergantung pada HTTP.
Mudah di-testing — service bisa ditest tanpa perlu menjalankan server HTTP.
Scalable — mudah ditambahkan lapisan baru seperti middleware, validasi, atau auth tanpa merusak struktur yang ada.
3. Implementasi: Refactor Todo List API
Sekarang kita praktikkan langsung. Kita akan membangun Todo List API dari nol dengan struktur folder yang benar, menggunakan Express dan MongoDB (Mongoose).
3.1 Setup Project
mkdir todo-api
cd todo-api
npm init -y
npm install express mongoose
npm install --save-dev nodemon
Buka package.json, tambahkan script berikut di bagian "scripts":
{
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
}
}
3.2 Struktur Folder Final
Berikut struktur folder yang akan kita buat:
todo-api/
├── src/
│ ├── config/
│ │ └── db.js
│ ├── models/
│ │ └── todo.model.js
│ ├── services/
│ │ └── todo.service.js
│ ├── controllers/
│ │ └── todo.controller.js
│ ├── routes/
│ │ └── todo.routes.js
│ ├── app.js
│ └── server.js
├── package.json
└── node_modules/
Buat semua folder ini terlebih dahulu:
mkdir -p src/config src/models src/services src/controllers src/routes
3.3 Config: Koneksi Database
src/config/db.js
const mongoose = require("mongoose");
async function connectDB() {
try {
// URI database masih ditulis langsung dulu di artikel ini.
// Di artikel berikutnya kita akan pindahkan ke file .env agar lebih aman.
const uri = "mongodb://127.0.0.1:27017/todo_api_db";
await mongoose.connect(uri);
console.log("MongoDB connected successfully");
} catch (error) {
console.error("MongoDB connection failed:", error.message);
process.exit(1);
}
}
module.exports = connectDB;
3.4 Model: Struktur Data Todo
src/models/todo.model.js
const mongoose = require("mongoose");
const todoSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
},
description: {
type: String,
default: "",
},
completed: {
type: Boolean,
default: false,
},
},
{
timestamps: true, // otomatis menambahkan createdAt & updatedAt
}
);
module.exports = mongoose.model("Todo", todoSchema);
3.5 Service: Business Logic
Service bertugas berkomunikasi dengan model dan menjalankan logika bisnis. Service tidak boleh tahu apa-apa soal req atau res — itu tugas controller.
src/services/todo.service.js
const Todo = require("../models/todo.model");
async function createTodo(data) {
const todo = new Todo({
title: data.title,
description: data.description,
});
return await todo.save();
}
async function getAllTodos() {
return await Todo.find().sort({ createdAt: -1 });
}
async function getTodoById(id) {
return await Todo.findById(id);
}
async function updateTodo(id, data) {
return await Todo.findByIdAndUpdate(
id,
{
title: data.title,
description: data.description,
completed: data.completed,
},
{ new: true, runValidators: true }
);
}
async function deleteTodo(id) {
return await Todo.findByIdAndDelete(id);
}
module.exports = {
createTodo,
getAllTodos,
getTodoById,
updateTodo,
deleteTodo,
};
3.6 Controller: Jembatan HTTP ke Service
Controller bertugas menerima request, memanggil service, lalu mengirimkan response. Semua error ditangani di sini agar server tidak crash.
src/controllers/todo.controller.js
const todoService = require("../services/todo.service");
async function createTodo(req, res) {
try {
const { title, description } = req.body;
if (!title) {
return res.status(400).json({
success: false,
message: "Title is required",
});
}
const todo = await todoService.createTodo({ title, description });
return res.status(201).json({
success: true,
message: "Todo created successfully",
data: todo,
});
} catch (error) {
return res.status(500).json({
success: false,
message: "Failed to create todo",
error: error.message,
});
}
}
async function getAllTodos(req, res) {
try {
const todos = await todoService.getAllTodos();
return res.status(200).json({
success: true,
message: "Todos retrieved successfully",
data: todos,
});
} catch (error) {
return res.status(500).json({
success: false,
message: "Failed to retrieve todos",
error: error.message,
});
}
}
async function getTodoById(req, res) {
try {
const { id } = req.params;
const todo = await todoService.getTodoById(id);
if (!todo) {
return res.status(404).json({
success: false,
message: "Todo not found",
});
}
return res.status(200).json({
success: true,
message: "Todo retrieved successfully",
data: todo,
});
} catch (error) {
return res.status(500).json({
success: false,
message: "Failed to retrieve todo",
error: error.message,
});
}
}
async function updateTodo(req, res) {
try {
const { id } = req.params;
const { title, description, completed } = req.body;
const updatedTodo = await todoService.updateTodo(id, {
title,
description,
completed,
});
if (!updatedTodo) {
return res.status(404).json({
success: false,
message: "Todo not found",
});
}
return res.status(200).json({
success: true,
message: "Todo updated successfully",
data: updatedTodo,
});
} catch (error) {
return res.status(500).json({
success: false,
message: "Failed to update todo",
error: error.message,
});
}
}
async function deleteTodo(req, res) {
try {
const { id } = req.params;
const deletedTodo = await todoService.deleteTodo(id);
if (!deletedTodo) {
return res.status(404).json({
success: false,
message: "Todo not found",
});
}
return res.status(200).json({
success: true,
message: "Todo deleted successfully",
data: deletedTodo,
});
} catch (error) {
return res.status(500).json({
success: false,
message: "Failed to delete todo",
error: error.message,
});
}
}
module.exports = {
createTodo,
getAllTodos,
getTodoById,
updateTodo,
deleteTodo,
};
3.7 Router: Mendefinisikan Endpoint
src/routes/todo.routes.js
const express = require("express");
const router = express.Router();
const todoController = require("../controllers/todo.controller");
router.post("/", todoController.createTodo);
router.get("/", todoController.getAllTodos);
router.get("/:id", todoController.getTodoById);
router.put("/:id", todoController.updateTodo);
router.delete("/:id", todoController.deleteTodo);
module.exports = router;
3.8 App: Menyatukan Semua Komponen
src/app.js
const express = require("express");
const todoRoutes = require("./routes/todo.routes");
const app = express();
// Parsing JSON body dari request
app.use(express.json());
// Mendaftarkan semua route todo dengan prefix /api/todos
app.use("/api/todos", todoRoutes);
// Route dasar untuk cek server hidup
app.get("/", (req, res) => {
res.json({ message: "Todo API is running" });
});
module.exports = app;
3.9 Server: Entry Point
src/server.js
const app = require("./app");
const connectDB = require("./config/db");
const PORT = 3000;
async function startServer() {
await connectDB();
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
}
startServer();
4. Menjalankan dan Menguji API
Pastikan MongoDB sudah berjalan di komputermu (lokal atau lewat MongoDB Atlas — sesuaikan uri di db.js jika pakai Atlas).
Jalankan server:
npm run dev
Kalau berhasil, terminal akan menampilkan:
MongoDB connected successfully
Server running on http://localhost:3000
Sekarang API sudah punya endpoint yang bisa dites lewat Postman atau curl:
Method | Endpoint | Fungsi |
|---|---|---|
POST |
| Membuat todo baru |
GET |
| Mengambil semua todo |
GET |
| Mengambil satu todo |
PUT |
| Mengupdate todo |
DELETE |
| Menghapus todo |
Contoh test dengan curl untuk membuat todo baru:
curl -X POST http://localhost:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"title": "Belajar struktur folder Express", "description": "Praktik controller-service-model"}'
Response yang diharapkan:
{
"success": true,
"message": "Todo created successfully",
"data": {
"_id": "665f1c2e8b1e2a1a2c3d4e5f",
"title": "Belajar struktur folder Express",
"description": "Praktik controller-service-model",
"completed": false,
"createdAt": "2026-07-18T02:00:00.000Z",
"updatedAt": "2026-07-18T02:00:00.000Z",
"__v": 0
}
}
5. Perbandingan Sebelum dan Sesudah
Aspek | Sebelum (Function-Based) | Sesudah (Controller-Service-Model) |
|---|---|---|
Endpoint HTTP | Tidak ada | Ada, via Express Router |
Penyimpanan data | Array di memori | MongoDB (Mongoose) |
Pemisahan logic | Tidak ada, semua campur | Terpisah jelas per lapisan |
Error handling | Tidak ada | Ditangani di controller dengan try-catch |
Kemudahan dikembangkan | Sulit, satu file makin besar | Mudah, tinggal tambah file per fitur |
Kemudahan testing | Sulit dipisah dari logic lain | Service bisa ditest terpisah |
6. Kesimpulan
Merestrukturisasi kode CRUD dari kumpulan function menjadi pola Controller-Service-Model adalah langkah pertama menuju backend yang scalable dan mudah dirawat. Dengan memisahkan tanggung jawab tiap lapisan, kode menjadi lebih rapi, mudah ditelusuri saat debugging, dan lebih siap dikembangkan lebih lanjut — misalnya menambahkan middleware, autentikasi, atau validasi input.
Struktur folder Todo List API yang sudah kita bangun di artikel ini akan menjadi fondasi untuk artikel selanjutnya. Langkah berikutnya adalah membuat API ini lebih aman dan rapi menggunakan middleware dan environment variable.
Lanjutkan ke artikel berikutnya: Middleware dan .env di Express.js: Cara Bikin API Lebih Aman dan Rapi — di artikel ini, konfigurasi .env dan error handler global akan ditambahkan langsung ke project Todo List yang sudah kita buat.
Artikel Terkait
Panduan Membuat Animasi Animated on Scroll di Website Anda
Pelajari cara membuat animasi menarik pada scroll di website Anda dengan tutorial ini. Tampilkan konten dengan cara yang lebih interaktif!
Cara Menangani Error dan HTTP Status Code yang Benar
cara menentukan HTTP status code yang benar untuk tiap jenis error, dan cara membuat custom Error
Tutorial Membuat REST API dengan Laravel yang Mudah dan Praktis
Pelajari langkah demi langkah cara membuat REST API menggunakan Laravel dengan mudah. Cocok untuk pemula dan pengembang berpengalaman.
