← Back to Blog

Article

Express.js dari Intermediate ke Senior: Praktik yang Wajib Dikuasai

Panduan lengkap dan mudah dipahami tentang Express.js: arsitektur, middleware, error handling, auth, observability, dan performance untuk aplikasi produksi.

2023-04-22·4 min read
Express.jsNode.jsBackendAPIArchitecture

Express.js terlihat simpel, tapi perbedaan kualitas aplikasi produksi ada pada detailnya: struktur, middleware, error handling, observability, dan keamanan. Artikel ini merangkum praktik yang perlu kamu kuasai untuk naik level dari intermediate ke senior.

1) Struktur project yang rapi

Minimal yang sehat:

bash
src/
  routes/
  controllers/
  services/
  repositories/
  middlewares/
  lib/
  app.ts

Intinya: pisahkan HTTP (routes/controllers) dari bisnis (services) dan data (repositories).

2) Middleware yang tepat sasaran

Middleware bukan hanya untuk auth. Gunakan untuk:

  • logging
  • validation
  • rate limit
  • error normalization

Contoh pipeline sederhana:

ts
app.use(requestId())
app.use(json())
app.use(logger())
app.use(routes)
app.use(notFound())
app.use(errorHandler())

3) Error handling yang konsisten

Jangan throw sembarangan. Buat error class dan standar response.

ts
class AppError extends Error {
  constructor(public status: number, message: string) {
    super(message)
  }
}
 
function errorHandler(err, req, res, next) {
  const status = err.status ?? 500
  res.status(status).json({
    error: err.message ?? "Internal error",
  })
}

Intinya: semua error harus keluar dengan format konsisten.

4) Validation di boundary

Validasi input di layer HTTP. Jangan menunggu sampai service.

Pakai zod/joi/yup untuk schema:

ts
const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

Intinya: reject request sebelum menyentuh bisnis.

5) Auth & authorization jelas

Pisahkan:

  • Authentication: siapa user‑nya
  • Authorization: boleh akses apa

Contoh:

ts
app.get("/admin", requireAuth, requireRole("admin"), handler)

Contoh implementasi sederhana:

ts
function requireAuth(req, res, next) {
  const user = req.user
  if (!user) return res.status(401).json({ error: "Unauthorized" })
  next()
}
 
const requireRole = (role) => (req, res, next) => {
  if (req.user?.role !== role) {
    return res.status(403).json({ error: "Forbidden" })
  }
  next()
}

Kenapa ini penting: tanpa pemisahan auth/role, endpoint mudah bocor dan sulit diaudit saat scope user bertambah.

6) Database access: jangan campur dengan controller

Controller hanya req -> res. Semua query di repository/service.

Bad:

ts
app.get("/users", async (req, res) => {
  const users = await db.query("SELECT ...")
  res.json(users)
})

Good:

ts
const users = await userService.list()
res.json(users)

Contoh repository:

ts
const userRepo = {
  list: () => db.query("SELECT id, name, role FROM users"),
}

Kenapa ini penting: pemisahan repo membuat query mudah diuji, di-cache, dan diganti tanpa merusak controller.

7) Observability: logs + metrics

Tambahkan:

  • request id
  • latency
  • error rate

Minimal log format:

ts
{ requestId, route, status, durationMs }

Intinya: tanpa log yang baik, debugging di produksi itu buta.

Contoh middleware:

ts
app.use((req, res, next) => {
  const start = Date.now()
  res.on("finish", () => {
    console.log({
      route: req.path,
      status: res.statusCode,
      durationMs: Date.now() - start,
    })
  })
  next()
})

Kenapa ini penting: tanpa log latency dan status, root cause error di produksi akan sulit ditemukan.

8) Performance yang benar

Hal yang sering dilupakan:

  • gzip/brotli
  • caching header
  • rate limit
  • slow query logging

Intinya: performa buruk sering datang dari query lambat dan payload berlebihan.

Contoh caching header untuk data yang jarang berubah:

ts
res.set("Cache-Control", "public, max-age=60")

Kenapa ini penting: caching mengurangi beban server dan membuat UI terasa lebih cepat.

9) Security dasar yang wajib

Checklist minimal:

  • helmet
  • cors yang ketat
  • rate limit
  • sanitasi input
  • hide stack trace di prod

Contoh setup:

ts
import helmet from "helmet"
import cors from "cors"
 
app.use(helmet())
app.use(cors({ origin: ["https://jeryl.id"] }))

Kenapa ini penting: security baseline menurunkan risiko exposed endpoint dan data leakage.

Contoh rate limit sederhana:

ts
import rateLimit from "express-rate-limit"
 
const limiter = rateLimit({
  windowMs: 60 * 1000,
  max: 120,
})
 
app.use("/api/", limiter)

Kenapa ini penting: rate limit mencegah abuse dan menjaga API tetap stabil.

10) Testing yang efektif

Minimal yang perlu:

  • unit test service
  • integration test endpoint kritis
  • mock DB

Jika waktu terbatas, test flow utama terlebih dulu.

Contoh test minimal (supertest):

ts
import request from "supertest"
import app from "../src/app"
 
it("GET /health", async () => {
  await request(app).get("/health").expect(200)
})

Kenapa ini penting: test sederhana mencegah regresi saat deploy cepat.

11) Graceful shutdown (sering dilupakan)

Supaya tidak ada request yang terputus saat deploy/restart:

ts
const server = app.listen(3000)
 
process.on("SIGTERM", () => {
  server.close(() => {
    process.exit(0)
  })
})

Kenapa ini penting: tanpa shutdown yang rapi, request bisa terputus saat deploy.

12) Pagination + filtering yang konsisten

Standarkan page, limit, dan search agar API mudah dipakai:

ts
app.get("/users", async (req, res) => {
  const page = Number(req.query.page ?? 1)
  const limit = Number(req.query.limit ?? 20)
  const offset = (page - 1) * limit
  const rows = await db.query("SELECT * FROM users LIMIT $1 OFFSET $2", [
    limit,
    offset,
  ])
  res.json({ page, limit, data: rows })
})

Kenapa ini penting: konsistensi pagination membuat frontend dan analytics lebih mudah dirawat.

13) Queue / background job (untuk task berat)

Gunakan queue untuk proses yang berat (email, export, report):

ts
import { Queue } from "bullmq"
 
const exportQueue = new Queue("export")
 
app.post("/export", async (req, res) => {
  await exportQueue.add("generate", { userId: req.user.id })
  res.json({ status: "queued" })
})

Kenapa ini penting: task berat tidak boleh memblok request utama.

Kesimpulan

Naik level di Express bukan soal trik, tapi disiplin: struktur yang jelas, middleware yang konsisten, error handling yang rapi, dan observability yang memadai. Jika fondasi ini kuat, aplikasi tetap stabil meski scale dan traffic naik.