Express.js #

Express.js adalah framework web Node.js yang paling banyak dipakai di dunia. Ia menjadi pondasi bagi ribuan aplikasi REST API, BFF, dan microservice. Ekosistemnya matang, komunitasnya luas, dan integrasi ke database, cache, message broker hampir selalu punya library resmi. Namun, di balik kepopulerannya, Express menyimpan satu titik lemah yang sering muncul di local development: perbedaan environment antar developer.

Kamu pasti pernah mengalami: di laptopmu aplikasi jalan mulus, tapi di laptop rekan kerja npm install gagal. Atau, kode yang kamu tulis di Node 20 tidak jalan karena ada yang masih pakai Node 16. Masalah seperti ini bukan salah Express, tapi salah setup environment. Docker adalah solusi paling pragmatis untuk masalah ini — kamu menyamakan runtime, dependency, dan service pendukung dalam satu file docker-compose.yml yang bisa dijalankan siapa pun.

Artikel ini membahas setup Docker Compose untuk local development Express.js secara lengkap: Dockerfile multi-stage yang optimal, docker-compose dengan Postgres dan Redis, hot reload dengan nodemon, routing modular, middleware, error handling, validasi, ORM, dan best practice yang sering diabaikan.

Prasyarat #

Pastikan sudah terinstall di host:

  • Docker dan Docker Compose versi terbaru (v2.20+)
  • Node.js 20+ — opsional, hanya untuk menjalankan test atau tooling di host
  • Editor favoritmu (VS Code, WebStorm, Neovim)
  • curl atau Postman — untuk testing endpoint

Struktur project Express yang diasumsikan:

express-app/
├── src/
│   ├── index.js
│   ├── routes/
│   ├── controllers/
│   ├── middleware/
│   ├── services/
│   ├── validators/
│   └── db/
├── tests/
├── package.json
├── package-lock.json
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env.example
└── .eslintrc.cjs

Catatan: Struktur di atas adalah pola MVC ringan. Kamu bisa menyesuaikannya dengan monorepo atau layered architecture sesuai kebutuhan tim.

Gunakan Node.js 20 LTS sebagai runtime minimum. Node 18 sudah end-of-life, dan banyak dependency modern sudah mulai mensyaratkan Node 20+. Image Alpine dipilih karena ukurannya kecil dan sudah termasuk glibc yang kompatibel dengan binary module seperti bcrypt dan sharp.

Dockerfile Multi-Stage #

Untuk Express, kita buat Dockerfile multi-stage. Tahap pertama (deps) berisi Node dengan devDependencies, tahap kedua (runner) berisi Node versi Alpine ramping untuk production. Untuk local development kita jalankan deps stage — sudah punya semua tools yang dibutuhkan, termasuk nodemon.

# syntax=docker/dockerfile:1.6
FROM node:20-alpine AS deps

WORKDIR /app

# Salin manifest lebih dulu untuk optimalkan cache layer
COPY package.json package-lock.json ./

# Install semua dependency, termasuk devDependencies (nodemon, eslint, jest)
RUN npm install --no-audit --no-fund

# Tahap runner untuk production
FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production \
    PORT=3000

# Buat user non-root untuk keamanan
RUN addgroup -S app && adduser -S app -G app

# Salin dependency dari stage deps
COPY --from=deps --chown=app:app /app/node_modules ./node_modules
COPY --chown=app:app package.json ./
COPY --chown=app:app src ./src

USER app

EXPOSE 3000

CMD ["node", "src/index.js"]

Penjelasan Tahapan #

Stage deps — berisi node_modules lengkap dengan devDependencies. Tahap ini yang dipakai untuk development. Cache layer di sini krusial: salin package.json dan package-lock.json lebih dulu, jalankan npm install, dan Docker akan cache layer dependency. Saat kamu edit kode, layer dependency tidak ikut ter-build ulang.

Stage runner — image ramping untuk production. Hanya menyertakan dependency production (NODE_ENV=production) dan source code. User non-root (app) penting untuk mencegah container berjalan sebagai root — kalau ada celah keamanan di aplikasi, penyerang tidak langsung mendapat akses root di host.

--no-audit --no-fund — mencegah npm menampilkan pesan audit dan funding yang tidak relevan di log build.

Jangan pernah gunakan node:latest atau node:20 tanpa tag. Tag tanpa versi akan menarik image acak saat build, dan bug bisa tiba-tiba muncul saat image di-pull ulang. Selalu gunakan tag LTS spesifik seperti node:20-alpine.

docker-compose.yml untuk Development #

# docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: deps        # gunakan stage deps (ada devDependencies)
    image: express-app:dev
    container_name: express-dev
    command: npm run dev
    working_dir: /app
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src
      - ./tests:/app/tests
      - /app/node_modules   # anonymous volume: lindungi node_modules
    environment:
      NODE_ENV: development
      PORT: 3000
      DATABASE_URL: postgres://app:dev@db:5432/express_dev
      REDIS_URL: redis://cache:6379
      JWT_SECRET: dev-secret-change-me
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    container_name: express-db
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: express_dev
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d express_dev"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    container_name: express-cache
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    ports:
      - "6379:6379"

volumes:
  db-data:

Penjelasan Service #

app — service Express. Baris target: deps memilih stage deps dari Dockerfile, sehingga node_modules lengkap dengan devDependencies ikut ter-copy. command: npm run dev menjalankan skrip dev dari package.json (yang kita set ke nodemon). Volume ./src:/app/src membuat source code di host langsung tercermin di container — nodemon akan mendeteksi perubahan dan restart server.

Anonymous volume /app/node_modules — baris kecil ini sangat penting. Tanpa volume ini, Docker akan menimpa node_modules di container dengan versi kosong dari host (yang biasanya tidak ada). Anonymous volume mempertahankan node_modules yang sudah di-install di image.

db — PostgreSQL 16 dengan healthcheck pg_isready. App tidak akan start sampai database benar-benar siap menerima koneksi. Volume db-data menyimpan data di named volume agar tidak hilang saat container di-restart. Folder db/init di-mount ke /docker-entrypoint-initdb.d untuk otomatis menjalankan SQL inisialisasi.

cache — Redis 7 untuk session, cache, dan rate limiting. Healthcheck redis-cli ping memastikan Redis hidup sebelum app start.

Diagram Arsitektur #

flowchart LR
    Dev[Developer<br/>di Host] -->|edit src/ via bind mount| App[Container: app<br/>Express + nodemon]
    App -->|query SELECT/INSERT| DB[(Postgres 16)]
    App -->|GET/SET cache| Cache[(Redis 7)]
    DB -.->|healthcheck<br/>pg_isready| HC1{Health Check}
    Cache -.->|healthcheck<br/>redis-cli ping| HC2{Health Check}
    HC1 -->|healthy| App
    HC2 -->|healthy| App
Selalu gunakan condition: service_healthy di depends_on daripada depends_on: - db biasa. Versi biasa hanya menunggu container start, bukan service siap. Healthcheck membuat app start tepat saat Postgres sudah menerima koneksi — tidak perlu sleep di entrypoint.

Hot Reload dengan Nodemon #

Nodemon adalah pilihan standar untuk hot reload di Express. Tambahkan ke devDependencies dan buat konfigurasi yang mengabaikan folder yang tidak boleh memicu restart.

package.json:

{
  "name": "express-app",
  "version": "1.0.0",
  "type": "commonjs",
  "scripts": {
    "dev": "nodemon --watch src --ext js,json",
    "start": "node src/index.js",
    "test": "jest --runInBand"
  },
  "dependencies": {
    "express": "^4.19.2",
    "pg": "^8.12.0",
    "ioredis": "^5.4.1",
    "zod": "^3.23.8",
    "helmet": "^7.1.0",
    "cors": "^2.8.5",
    "express-rate-limit": "^7.4.0",
    "pino": "^9.3.2",
    "pino-http": "^10.2.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.4",
    "jest": "^29.7.0",
    "supertest": "^7.0.0"
  }
}

nodemon.json (opsional, untuk kontrol lebih):

{
  "watch": ["src"],
  "ext": "js,json",
  "ignore": ["src/**/*.test.js", "node_modules"],
  "delay": 500
}

.dockerignore:

node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
coverage
.nyc_output
tests
Dockerfile
docker-compose.yml
.dockerignore
README.md

File .dockerignore mencegah file yang tidak perlu masuk ke build context. Tanpa file ini, Docker akan menyalin node_modules host ke dalam image — yang biasanya platform-specific dan akan konflik dengan versi Alpine.


Routing Modular #

Express menganut pola routing yang fleksibel. Untuk project yang mulai besar, pecah routing berdasarkan resource agar tetap maintainable.

src/index.js — entrypoint:

const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const pinoHttp = require('pino-http');
const rateLimit = require('express-rate-limit');

const logger = require('./services/logger');
const errorHandler = require('./middleware/errorHandler');
const notFound = require('./middleware/notFound');

const userRoutes = require('./routes/users');
const authRoutes = require('./routes/auth');

const app = express();

// ANTI-PATTERN: lupa pakai middleware security
// app.use(express.json());

// BENAR: urutan middleware yang aman
app.use(helmet());                         // security headers
app.use(cors({ origin: process.env.CORS_ORIGIN || '*' }));
app.use(express.json({ limit: '1mb' }));   // body parser dengan limit
app.use(pinoHttp({ logger }));             // request logging

// Rate limit di level global
app.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false
}));

// Routes
app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/users', userRoutes);

// Health check
app.get('/health', (req, res) => res.json({ status: 'ok' }));

// 404 dan error handler HARUS di paling akhir
app.use(notFound);
app.use(errorHandler);

const PORT = process.env.PORT || 3000;
app.listen(PORT, '0.0.0.0', () => {
  logger.info(`Server running on port ${PORT}`);
});

module.exports = app;

src/routes/users.js:

const express = require('express');
const { getUser, listUsers, createUser, updateUser, deleteUser } = require('../controllers/users');
const { validate } = require('../middleware/validate');
const { createUserSchema, updateUserSchema } = require('../validators/user');

const router = express.Router();

router.get('/', listUsers);
router.get('/:id', getUser);
router.post('/', validate(createUserSchema), createUser);
router.put('/:id', validate(updateUserSchema), updateUser);
router.delete('/:id', deleteUser);

module.exports = router;

Tabel Method HTTP yang Umum #

Method Path Tujuan Idempotent
GET /users List semua user Ya
GET /users/:id Ambil satu user Ya
POST /users Buat user baru Tidak
PUT /users/:id Update user (replace) Ya
PATCH /users/:id Update sebagian field Tidak
DELETE /users/:id Hapus user Ya

Middleware Custom #

Middleware adalah jantung Express. Fungsi (req, res, next) yang bisa mencegat request sebelum sampai ke handler. Tiga middleware yang hampir selalu kamu butuhkan: validator, logger, dan error handler.

src/middleware/validate.js — validasi body dengan Zod:

const { ZodError } = require('zod');

function validate(schema) {
  return (req, res, next) => {
    try {
      // ANTI-PATTERN: validasi manual satu-satu
      // if (!req.body.email) return res.status(400).json({...});
      // if (!req.body.password || req.body.password.length < 8) return ...

      // BENAR: pakai schema-based validation
      req.body = schema.parse(req.body);
      next();
    } catch (err) {
      if (err instanceof ZodError) {
        return res.status(400).json({
          error: 'ValidationError',
          details: err.issues.map(i => ({
            path: i.path.join('.'),
            message: i.message
          }))
        });
      }
      next(err);
    }
  };
}

module.exports = { validate };

src/validators/user.js:

const { z } = require('zod');

const createUserSchema = z.object({
  body: z.object({
    email: z.string().email(),
    name: z.string().min(2).max(100),
    password: z.string().min(8).max(128),
    role: z.enum(['user', 'admin']).default('user')
  })
});

const updateUserSchema = z.object({
  body: z.object({
    name: z.string().min(2).max(100).optional(),
    email: z.string().email().optional()
  })
});

module.exports = { createUserSchema, updateUserSchema };

src/middleware/errorHandler.js — central error handler:

const logger = require('../services/logger');

// ANTI-PATTERN: tangkap error di setiap route satu-satu
// router.get('/users/:id', async (req, res) => {
//   try { ... } catch (e) { res.status(500).send(e) }
// });

// BENAR: pakai error middleware terpusat
function errorHandler(err, req, res, next) {
  // Default error
  const status = err.statusCode || 500;
  const code = err.code || 'InternalServerError';

  // Log error lengkap di server
  logger.error({
    err,
    requestId: req.id,
    path: req.path,
    method: req.method
  }, 'request failed');

  // Jangan bocorkan detail internal ke client
  const response = {
    error: code,
    message: status < 500 ? err.message : 'Something went wrong'
  };

  if (err.details) response.details = err.details;

  res.status(status).json(response);
}

module.exports = errorHandler;

src/middleware/notFound.js:

function notFound(req, res) {
  res.status(404).json({
    error: 'NotFound',
    message: `Route ${req.method} ${req.path} tidak ditemukan`
  });
}

module.exports = notFound;
Express mengenali error handler dari signature (err, req, res, next) — empat parameter. Pastikan jumlah parameter tepat empat, karena Express menggunakan .length function untuk membedakan error handler dari middleware biasa. Kalau kamu tulis (err, req, res), Express akan mengabaikannya.

Database Access dengan pg (node-postgres) #

Express tidak punya ORM bawaan — itu keputusan desain, bukan kekurangan. Kamu bebas memilih: Prisma, Knex, Sequelize, Drizzle, atau raw pg untuk kontrol penuh. Untuk contoh ini kita pakai pg Pool.

src/db/pool.js:

const { Pool } = require('pg');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,                          // max connection di pool
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 5_000
});

pool.on('error', (err) => {
  // Log error agar tidak crash silent
  console.error('Unexpected error on idle PG client', err);
});

module.exports = { pool };

src/controllers/users.js:

const { pool } = require('../db/pool');
const { NotFoundError, ValidationError } = require('../errors');

async function listUsers(req, res, next) {
  try {
    const limit = Math.min(parseInt(req.query.limit) || 20, 100);
    const offset = parseInt(req.offset) || 0;

    const result = await pool.query(
      'SELECT id, email, name, role, created_at FROM users ORDER BY id LIMIT $1 OFFSET $2',
      [limit, offset]
    );

    res.json({ data: result.rows, total: result.rowCount });
  } catch (err) {
    next(err);
  }
}

async function getUser(req, res, next) {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) throw new ValidationError('id harus angka');

    const result = await pool.query(
      'SELECT id, email, name, role, created_at FROM users WHERE id = $1',
      [id]
    );
    if (result.rowCount === 0) throw new NotFoundError(`User ${id} tidak ditemukan`);

    res.json({ data: result.rows[0] });
  } catch (err) {
    next(err);
  }
}

module.exports = { listUsers, getUser };

src/errors/index.js — custom error classes:

class AppError extends Error {
  constructor(message, statusCode = 500, code = 'AppError') {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
  }
}

class NotFoundError extends AppError {
  constructor(message = 'Resource tidak ditemukan') {
    super(message, 404, 'NotFound');
  }
}

class ValidationError extends AppError {
  constructor(message, details) {
    super(message, 400, 'ValidationError');
    this.details = details;
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Tidak terautentikasi') {
    super(message, 401, 'Unauthorized');
  }
}

module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError };
Jangan pernah return stack trace atau detail error internal (seperti SQL error message) ke client di production. Di development boleh, tapi di production log error lengkap di server dan kembalikan response generik. Error handler di atas sudah memfilter ini: untuk status 5xx, response hanya menampilkan “Something went wrong”.

Testing dengan Jest dan Supertest #

Testing Express sering luput. Padahal dengan supertest, kamu bisa menguji endpoint HTTP tanpa start server beneran — Supertest mem-bind ke ephemeral port.

tests/users.test.js:

const request = require('supertest');
const app = require('../src/index');
const { pool } = require('../src/db/pool');

describe('GET /api/v1/users', () => {
  afterAll(async () => {
    await pool.end();
  });

  it('mengembalikan list user', async () => {
    const res = await request(app).get('/api/v1/users');

    expect(res.status).toBe(200);
    expect(res.body).toHaveProperty('data');
    expect(Array.isArray(res.body.data)).toBe(true);
  });

  it('mengembalikan 404 untuk user yang tidak ada', async () => {
    const res = await request(app).get('/api/v1/users/99999');
    expect(res.status).toBe(404);
    expect(res.body.error).toBe('NotFound');
  });
});

Test jalan tanpa harus start Postgres? Bisa dengan mocking. Tapi idealnya kamu punya test database yang terisolasi. Untuk development, tambahkan service db-test di Compose dengan database terpisah.


Build dan Run #

Perintah sehari-hari:

# Build image pertama kali (atau setelah ubah Dockerfile/package.json)
docker compose build

# Jalankan semua service di background
docker compose up -d

# Lihat log app secara real-time
docker compose logs -f app

# Restart hanya service app (misalnya setelah ubah env var)
docker compose restart app

# Stop semua service
docker compose down

# Hapus container + volume (data Postgres hilang!)
docker compose down -v

Akses setelah up:

Service URL Keterangan
App http://localhost:3000 API utama
Health check http://localhost:3000/health Status server
Postgres localhost:5432 User app, password dev
Redis `localhost:6379 Cache
pgAdmin (opsional) http://localhost:5050 UI database, tambahkan manual

Test endpoint dengan curl:

curl -X POST http://localhost:3000/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Test User","password":"password123"}'
Gunakan docker compose logs --tail=100 -f app untuk melihat 100 baris log terakhir dan tetap follow stream baru. Kombinasikan dengan | grep ERROR untuk filter error saja — ini cara cepat debug saat ada request yang gagal.

Security Checklist #

Express sangat fleksibel, tapi juga sangat “tangan kosong” — tidak ada proteksi bawaan. Tambahkan middleware ini di awal pipeline:

Middleware Fungsi Konfigurasi Penting
helmet Set security headers (CSP, HSTS, X-Frame-Options) app.use(helmet())
cors Atur cross-origin policy origin: ['https://app.example.com']
express-rate-limit Cegah brute force dan abuse windowMs, max
express.json({ limit }) Batasi ukuran body limit: '1mb'
express-mongo-sanitize Anti NoSQL injection Untuk app MongoDB

Urutan middleware krusial. Yang harus paling awal: helmet, lalu CORS, lalu body parser, lalu logger, lalu rate limiter, lalu router. Error handler dan 404 di akhir.

// Urutan yang benar
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '1mb' }));
app.use(pinoHttp({ logger }));
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
// ... routers
app.use(notFound);
app.use(errorHandler);

Perbandingan Rate Limiting Strategies #

Strategy Kelebihan Kekurangan Cocok Untuk
Global limiter Sederhana, satu config Tidak adil untuk endpoint berat API kecil
Per-route limiter Kontrol granular Repetitif API publik besar
User-based limiter Adil per user Butuh auth di awal API dengan login
IP-based limiter Mudah diterapkan VPN/proxy bisa bypass Anti scraper

API Versioning #

Suatu saat API-mu akan berubah. Cara terbersih untuk handle perubahan adalah versioning lewat path: /api/v1/..., /api/v2/.... Tidak perlu hapus endpoint lama — biarkan ia coexist sampai client migrasi.

// src/routes/v1/index.js
const router = require('express').Router();
router.use('/users', require('./users'));
router.use('/auth', require('./auth'));
module.exports = router;

// src/routes/v2/index.js
const router = require('express').Router();
router.use('/users', require('./users'));   // struktur baru
module.exports = router;

// src/index.js
app.use('/api/v1', require('./routes/v1'));
app.use('/api/v2', require('./routes/v2'));
Alternatif versioning lain: header Accept: application/vnd.myapi.v2+json, atau subdomain v2.api.example.com. Path-based adalah yang paling sederhana dan paling jelas untuk dilihat di log, browser URL, dan Postman. Pilih salah satu dan konsisten di seluruh API-mu.

Best Practice #

1. Pakai Anonymous Volume untuk node_modules #

volumes:
  - /app/node_modules   # JANGAN hilangkan baris ini

Tanpa volume ini, Docker akan menimpa node_modules di container dengan versi kosong dari host (yang biasanya tidak ada). Express tidak akan start.

2. Pakai Healthcheck #

Postgres dan Redis harus punya healthcheck. App start dengan condition: service_healthy di depends_on. Jangan pakai sleep di entrypoint — itu tanda smell.

3. Pisahkan Dockerfile Dev dan Prod #

File di atas fokus ke development. Untuk production, tambahkan stage runner yang hanya copy dependency production (lihat Dockerfile multi-stage di atas). Atau buat Dockerfile.dev dan Dockerfile.prod terpisah.

4. Logging ke stdout/stderr #

Gunakan pino atau winston dengan transport stdout. Docker menangkap stdout/stderr via docker compose logs. Jangan log ke file — itu akan mengisi disk container.

5. Validasi dengan Schema, Bukan If-Else #

Zod, Joi, atau class-validator. Skema reusable, dokumentasi otomatis via TypeScript inference, dan tidak ada duplikasi validasi di controller.

6. Error Handler Terpusat #

Setiap next(err) di route akan sampai ke error handler. Ini mengurangi try-catch boilerplate dan memastikan response error konsisten.

7. Pakai Non-Root User #

Stage runner di Dockerfile membuat user app dan menjalankan container sebagai user tersebut. Mencegah eskalasi privilege jika aplikasi dieksploitasi.


Troubleshooting #

Container Restart Terus (CrashLoopBackOff) #

Cek log:

docker compose logs --tail=50 app

Penyebab umum: DATABASE_URL salah, node_modules hilang, port bentrok. Verifikasi environment variable dan urutan start dengan docker compose ps.

Hot Reload Tidak Jalan #

Pastikan:

  • nodemon ada di devDependencies
  • Volume ./src:/app/src di-mount dengan benar
  • File yang diedit berada di luar folder yang di-ignore (ignore: ['src/**/*.test.js'] bisa skip file test)

Test: edit file di host, jalankan docker compose logs -f app — harusnya ada pesan restart dari nodemon.

pg_isready Gagal #

Healthcheck salah konfigurasi. Cek user, database name, dan password di pg_isready cocok dengan environment di service db.

docker compose exec db pg_isready -U app -d express_dev

Redis Connection Refused #

Healthcheck redis-cli ping harusnya return PONG. Cek env REDIS_URL=redis://cache:6379 (hostname adalah nama service, bukan localhost).


Ringkasan #

  • Express ideal untuk local development yang konsisten — Postgres dan Redis berjalan sebagai container, bukan install manual.
  • Dockerfile multi-stage: stage deps untuk development (ada devDependencies), stage runner untuk production (image ramping, non-root user).
  • Anonymous volume /app/node_modules wajib — mencegah host menimpa dependency container.
  • Healthcheck di Postgres dan Redis + depends_on: condition: service_healthy agar app start setelah dependency siap.
  • Hot reload dengan nodemon + bind mount source code. Edit di host, restart otomatis di container.
  • Routing modular — pisahkan per resource, mount di /api/v1/<resource> agar struktur tetap jelas saat project membesar.
  • Middleware: helmet (security), cors (origin policy), express.json({ limit }) (body parser dengan limit), pino-http (logger), express-rate-limit (anti abuse).
  • Validasi schema-based dengan Zod/Joi — lebih aman, reusable, dan konsisten dibanding if-else manual.
  • Error handler terpusat dengan custom error classes (NotFoundError, ValidationError, UnauthorizedError). Pakai next(err) di route, tangkap di middleware.
  • API versioning lewat path /api/v1, /api/v2 — cara paling jelas untuk evolusi API tanpa breaking client.
  • Logging ke stdout, bukan file. Docker handle log rotation dan agregasi.
  • Security headers + rate limit adalah minimum, bukan opsional. Express tidak memproteksi apapun secara default.
  • Pisahkan Dockerfile dev dan prod. Development fokus DX (hot reload, devDeps), production fokus size dan security.
  • Troubleshooting paling sering: container crashloop (cek log), hot reload mati (cek volume), pg/redis refused (cek healthcheck).

← Sebelumnya: Rocket   Berikutnya: NestJS →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact