Middleware dan .env di Express.js: Cara Bikin API Lebih Aman dan Rapi

Tutorial
Jul 18, 2026
10 mnt baca
20 tayangan
Middleware dan .env di Express.js: Cara Bikin API Lebih Aman dan Rapi
A

Admin

Penulis Artikel

Middleware dan .env di Express.js: Cara Bikin API Lebih Aman dan Rapi

Di artikel sebelumnya, kita sudah merestrukturisasi Todo List API dari kode function-based menjadi pola Controller-Service-Model. API-nya sudah berjalan, tapi kalau kita perhatikan lebih dekat, masih ada beberapa masalah tersembunyi:

  • Connection string MongoDB masih hardcode langsung di db.js.

  • Tidak ada logging untuk memantau request yang masuk.

  • Kalau endpoint yang diakses tidak terdaftar, Express akan mengirim HTML error bawaan yang tidak konsisten dengan format response JSON kita.

  • Tidak ada tempat terpusat untuk menangani error yang tidak tertangkap.

Artikel ini akan membahas apa itu middleware, cara membuat custom middleware, dan cara mengelola konfigurasi dengan .env — langsung dipraktikkan pada project Todo List API yang sudah kita bangun.

Artikel ini adalah bagian kedua dari seri “Backend Node.js untuk Pemula”. Pastikan kamu sudah mengikuti artikel pertama: Cara Restrukturisasi CRUD Node.js karena kode di artikel ini melanjutkan struktur folder yang sama. Setelah ini, lanjutkan ke Cara Menangani Error dan HTTP Status Code yang Benar di Express.js.


1. Apa Itu Middleware?

Middleware adalah fungsi yang berada di antara request masuk dan response dikirim. Setiap middleware punya akses ke req, res, dan fungsi next() untuk meneruskan proses ke middleware/handler berikutnya.

Bentuk dasar sebuah middleware selalu seperti ini:

function namaMiddleware(req, res, next) {
  // lakukan sesuatu dengan req/res
  next(); // lanjutkan ke middleware/handler berikutnya
}

Kalau next() tidak dipanggil, request akan menggantung selamanya — ini kesalahan paling umum pemula saat pertama kali membuat middleware.

Analogi: bayangkan kamu masuk ke sekolah lewat gerbang. Sebelum sampai kelas, kamu harus lewat beberapa “pos” dulu: pos satpam (cek seragam), lalu pos absen (scan kartu pelajar), baru boleh masuk ke kelas. Setiap pos ini seperti middleware — mereka memeriksa atau memproses sesuatu sebelum mengizinkanmu lanjut ke pos berikutnya. Kalau satpam tidak bilang “silakan lanjut” (ini fungsi next()), kamu akan berdiri terus di gerbang dan tidak pernah sampai ke kelas. Itulah kenapa lupa memanggil next() membuat request “nyangkut” dan tidak pernah dapat response.

Alur request dengan middleware kurang lebih seperti ini:

Request masuk
   │
   ▼
Middleware 1 (misal: logger)  →  next()
   │
   ▼
Middleware 2 (misal: express.json())  →  next()
   │
   ▼
Router  →  Controller  →  Service  →  Model
   │
   ▼
Response dikirim

Kalau route tidak ditemukan / terjadi error tak terduga:
   │
   ▼
Middleware notFound / errorHandler (di paling akhir)

Middleware bisa dipasang secara global (berlaku untuk semua route) atau spesifik untuk route tertentu. Kita akan praktikkan dua-duanya.


2. Mengelola Konfigurasi dengan .env

Saat ini, connection string MongoDB ditulis langsung di dalam kode (src/config/db.js). Ini bermasalah karena:

  1. Kalau project di-push ke GitHub, kredensial database ikut ter-expose.

  2. Sulit membedakan konfigurasi antara development, staging, dan production.

  3. Setiap kali ganti environment, kamu harus edit langsung file kode.

Analogi: anggap connection string database itu seperti kunci rumah. Kalau kamu tulis kode rumah di secarik kertas lalu kertas itu ditempel di depan pintu (alias hardcode di kode program yang di-push ke GitHub), siapa pun yang lewat bisa lihat dan masuk ke rumahmu. Solusinya, kunci itu kamu simpan di tempat terpisah yang tidak ikut “dipamerkan” ke publik — itulah fungsi file .env. File ini seperti laci rahasia yang isinya cuma boleh diketahui kamu dan aplikasimu sendiri, sementara .env.example ibarat “daftar kunci apa saja yang dibutuhkan” tanpa menyebutkan kode kuncinya — berguna untuk kasih tahu temanmu apa saja yang perlu mereka siapkan sendiri.

Solusinya adalah menyimpan semua nilai konfigurasi di file .env, lalu membacanya lewat process.env.

2.1 Install dotenv

npm install dotenv

2.2 Buat File .env

Buat file .env di root project (sejajar dengan package.json):

.env

PORT=3000
MONGODB_URI=mongodb://127.0.0.1:27017/todo_api_db
NODE_ENV=development

2.3 Buat File .env.example

File .env tidak boleh ikut di-commit ke Git (akan kita atur di .gitignore), tapi teman satu timmu perlu tahu variabel apa saja yang dibutuhkan. Solusinya buat .env.example sebagai template, tanpa nilai rahasia:

.env.example

PORT=3000
MONGODB_URI=mongodb://127.0.0.1:27017/todo_api_db
NODE_ENV=development

2.4 Buat/Update .gitignore

.gitignore

node_modules/
.env

2.5 Load dotenv di Entry Point

dotenv harus di-load paling awal, sebelum module lain yang membutuhkan process.env dijalankan. Tempat paling aman adalah di baris pertama server.js.

src/server.js (update)

require("dotenv").config();

const app = require("./app");
const connectDB = require("./config/db");

const PORT = process.env.PORT || 3000;

async function startServer() {
  await connectDB();

  app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
    console.log(`Environment: ${process.env.NODE_ENV}`);
  });
}

startServer();

2.6 Update Config Database

Sekarang ganti hardcoded URI dengan process.env.MONGODB_URI:

src/config/db.js (update penuh)

const mongoose = require("mongoose");

async function connectDB() {
  try {
    const uri = process.env.MONGODB_URI;

    if (!uri) {
      throw new Error("MONGODB_URI is not defined in .env file");
    }

    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. Membuat Custom Middleware

Sekarang kita akan membuat 3 middleware yang paling umum dibutuhkan hampir semua project: logger, notFound handler, dan global error handler. Ketiganya kita taruh di folder baru src/middlewares/.

mkdir -p src/middlewares

3.1 Logger Middleware

Middleware ini mencatat setiap request yang masuk: method, URL, dan waktu prosesnya. Sangat membantu saat debugging.

Analogi: logger ini seperti buku tamu di pos satpam sekolah. Setiap ada orang lewat gerbang, satpam mencatat: jam berapa datang, siapa yang datang, dan tujuannya ke mana. Kalau suatu hari ada masalah (misalnya barang hilang), kamu tinggal buka buku tamu untuk lacak siapa saja yang lewat dan kapan. Begitu juga logger — kalau nanti ada bug atau server tiba-tiba lambat, log ini yang pertama kamu cek untuk tahu request mana yang bermasalah.

src/middlewares/logger.middleware.js

function logger(req, res, next) {
  const start = Date.now();
  const { method, originalUrl } = req;

  res.on("finish", () => {
    const duration = Date.now() - start;
    console.log(
      `[${new Date().toISOString()}] ${method} ${originalUrl} ${res.statusCode} - ${duration}ms`
    );
  });

  next();
}

module.exports = logger;

3.2 Not Found Middleware

Middleware ini menangkap semua request ke endpoint yang tidak terdaftar, lalu mengembalikan response JSON yang konsisten dengan format response API kita — bukan halaman HTML error bawaan Express.

src/middlewares/notFound.middleware.js

function notFound(req, res, next) {
  res.status(404).json({
    success: false,
    message: `Route ${req.method} ${req.originalUrl} not found`,
  });
}

module.exports = notFound;

3.3 Global Error Handler Middleware

Ini adalah middleware khusus untuk menangkap error yang dilempar dari mana pun di aplikasi (controller, service, atau middleware lain). Express mengenali middleware error handler dari jumlah parameternya yang harus 4: (err, req, res, next).

src/middlewares/errorHandler.middleware.js

function errorHandler(err, req, res, next) {
  console.error(err.stack);

  const statusCode = res.statusCode !== 200 ? res.statusCode : 500;

  res.status(statusCode).json({
    success: false,
    message: err.message || "Internal Server Error",
    // Stack trace hanya ditampilkan saat development, jangan di production
    stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
  });
}

module.exports = errorHandler;

Catatan: Versi error handler ini masih dasar. Standardisasi status code per jenis error (400, 401, 403, 404, 409, dst) dan custom Error class akan dibahas lebih dalam di artikel selanjutnya: Cara Menangani Error dan HTTP Status Code yang Benar di Express.js.


4. Memasang Middleware di app.js

Urutan pemasangan middleware itu penting. Middleware seperti logger dan express.json() harus dipasang di awal (sebelum route), sedangkan notFound dan errorHandler harus dipasang paling akhir (setelah semua route).

src/app.js (update penuh)

const express = require("express");
const todoRoutes = require("./routes/todo.routes");
const logger = require("./middlewares/logger.middleware");
const notFound = require("./middlewares/notFound.middleware");
const errorHandler = require("./middlewares/errorHandler.middleware");

const app = express();

// Middleware global — urutan pemasangan penting!
app.use(logger);
app.use(express.json());

// Route dasar untuk cek server hidup
app.get("/", (req, res) => {
  res.json({ message: "Todo API is running" });
});

// Mendaftarkan semua route todo dengan prefix /api/todos
app.use("/api/todos", todoRoutes);

// Middleware notFound — menangkap route yang tidak terdaftar
app.use(notFound);

// Middleware errorHandler — HARUS dipasang paling akhir
app.use(errorHandler);

module.exports = app;

5. Memanfaatkan Error Handler di Controller

Dengan adanya errorHandler global, controller sebenarnya tidak perlu lagi menulis blok catch yang panjang di setiap function. Cukup lempar error dengan next(error), dan errorHandler akan menanganinya secara terpusat.

src/controllers/todo.controller.js (update penuh)

const todoService = require("../services/todo.service");

async function createTodo(req, res, next) {
  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) {
    next(error);
  }
}

async function getAllTodos(req, res, next) {
  try {
    const todos = await todoService.getAllTodos();

    return res.status(200).json({
      success: true,
      message: "Todos retrieved successfully",
      data: todos,
    });
  } catch (error) {
    next(error);
  }
}

async function getTodoById(req, res, next) {
  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) {
    next(error);
  }
}

async function updateTodo(req, res, next) {
  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) {
    next(error);
  }
}

async function deleteTodo(req, res, next) {
  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) {
    next(error);
  }
}

module.exports = {
  createTodo,
  getAllTodos,
  getTodoById,
  updateTodo,
  deleteTodo,
};

Perhatikan perbedaannya dengan versi di artikel sebelumnya: setiap function sekarang menerima parameter ketiga next, dan blok catch yang tadinya mengirim res.status(500).json(...) secara manual, sekarang cukup memanggil next(error). Error akan otomatis ditangkap oleh errorHandler di app.js.


6. Struktur Folder Setelah Artikel Ini

todo-api/
├── src/
│   ├── config/
│   │   └── db.js
│   ├── middlewares/
│   │   ├── logger.middleware.js
│   │   ├── notFound.middleware.js
│   │   └── errorHandler.middleware.js
│   ├── models/
│   │   └── todo.model.js
│   ├── services/
│   │   └── todo.service.js
│   ├── controllers/
│   │   └── todo.controller.js
│   ├── routes/
│   │   └── todo.routes.js
│   ├── app.js
│   └── server.js
├── .env
├── .env.example
├── .gitignore
└── package.json

7. Menjalankan dan Menguji Ulang API

npm run dev

Terminal sekarang akan menampilkan environment yang aktif:

MongoDB connected successfully
Server running on http://localhost:3000
Environment: development

Tes 1 — Logger middleware. Akses endpoint apa pun (misalnya GET /api/todos), lalu perhatikan terminal. Kamu akan melihat log seperti ini:

[2026-07-18T02:10:00.000Z] GET /api/todos 200 - 47ms
test-logger

Tes 2 — Not Found middleware. Coba akses endpoint yang tidak ada, misalnya:

curl http://localhost:3000/api/unknown

Response yang diharapkan (JSON, bukan HTML error bawaan Express):

{
  "success": false,
  "message": "Route GET /api/unknown not found"
}
tes error route not-found

Tes 3 — Error handler. Coba akses todo dengan ID yang formatnya tidak valid untuk MongoDB, misalnya:

curl http://localhost:3000/api/todos/id-tidak-valid

Karena id-tidak-valid bukan ObjectId yang valid, Mongoose akan melempar error, lalu ditangkap oleh errorHandler:

{
  "success": false,
  "message": "Cast to ObjectId failed for value \"id-tidak-valid\" (type string) at path \"_id\" for model \"Todo\""
}
tes error id not valid

Jika Anda perhatikan, setelah pesan message terdapat teks penjelasan yang cukup panjang yang disebut stack. Tidak perlu khawatir, karena saat ini kita masih berada pada tahap development. Nantinya, setelah seluruh fitur selesai dikembangkan dan aplikasi siap untuk production, kita akan menyembunyikan informasi stack tersebut agar tidak ditampilkan kepada pengguna.


8. Perbandingan Sebelum dan Sesudah

Aspek

Sebelum

Sesudah

Konfigurasi database

Hardcode di db.js

Dari .env via process.env

Monitoring request

Tidak ada

Tercatat otomatis lewat logger middleware

Endpoint tidak terdaftar

HTML error bawaan Express

JSON response konsisten (404)

Error tak terduga

Ditangani manual di tiap controller

Ditangani terpusat oleh errorHandler

Keamanan kredensial

Ikut ter-commit ke Git

Disembunyikan lewat .gitignore


9. Kesimpulan

Dengan menambahkan middleware dan .env, Todo List API kita sekarang jauh lebih aman dan mudah dipantau: kredensial tidak lagi hardcode, setiap request tercatat lewat logger, endpoint yang salah memberi response konsisten, dan error tak terduga ditangani di satu tempat terpusat.

Tapi format error response yang kita buat di artikel ini masih sederhana — semua error dikembalikan dengan pesan generik dan status code seadanya. Di artikel selanjutnya, kita akan memperdalam bagaimana menentukan HTTP status code yang benar untuk tiap jenis error, dan membuat custom Error class agar response error lebih terstruktur dan mudah diprediksi oleh tim frontend.

Lanjutkan ke artikel berikutnya: Cara Menangani Error dan HTTP Status Code yang Benar di Express.js

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