Cara Restrukturisasi CRUD Node.js: Dari Function ke Folder Controller-Service-Model

Tutorial
Jul 18, 2026
8 mnt baca
20 tayangan
Cara Restrukturisasi CRUD Node.js: Dari Function ke Folder Controller-Service-Model
A

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:

  1. Tidak ada endpoint HTTP — belum bisa diakses lewat browser, Postman, atau frontend.

  2. Data disimpan di memori — hilang setiap server restart.

  3. Semua logika bercampur — jika nanti ditambah validasi, koneksi database, dan response HTTP, satu file ini akan membengkak jadi ratusan baris.

  4. Susah di-testing — logika bisnis (business logic) menyatu dengan logika penyimpanan data.

  5. 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

POST /api/todostodoController.createTodo

Controller

Menangani request & response HTTP (menerima req, mengembalikan res)

Ambil req.body, panggil service, kirim res.json()

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 todos

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

/api/todos

Membuat todo baru

GET

/api/todos

Mengambil semua todo

GET

/api/todos/:id

Mengambil satu todo

PUT

/api/todos/:id

Mengupdate todo

DELETE

/api/todos/:id

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!

Baca Artikel
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

Baca Artikel
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.

Baca Artikel