Middleware dan .env di Express.js: Cara Bikin API Lebih Aman dan Rapi
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:
Kalau project di-push ke GitHub, kredensial database ikut ter-expose.
Sulit membedakan konfigurasi antara development, staging, dan production.
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
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 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\""
}Jika Anda perhatikan, setelah pesan
messageterdapat 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!
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.
