Node.js Best Practices — Kuasai Pengembangan Backend
Pengenalan: Membangun Scalable Backend Applications
Node.js telah menjadi runtime pilihan untuk membangun aplikasi backend yang dapat diskalakan. Model I/O berbasis event dan non-blocking membuatnya sempurna untuk aplikasi intensif I/O. Panduan komprehensif ini mencakup best practices untuk membangun aplikasi Node.js grade production.
Daftar Isi
- Pengenalan
- Project Structure
- Asynchronous Programming
- Error Handling
- Middleware Pattern
- Environment Configuration
- Security Best Practices
- Database Optimization
- Performance Optimization
- Testing
- Logging dan Monitoring
- Deployment
Struktur Proyek
Mengorganisir proyek Node.js Kamu dengan benar memastikan pemeliharaan dan skalabilitas saat aplikasi Kamu berkembang.
Tata Letak Proyek yang Direkomendasikan
Struktur yang terorganisir dengan baik memisahkan masalah dan membuat kode lebih mudah dipahami dan diuji.
my-app/
├── src/
│ ├── controllers/ # Request handlers
│ ├── services/ # Business logic
│ ├── models/ # Database models
│ ├── routes/ # Route definitions
│ ├── middleware/ # Express middleware
│ ├── utils/ # Utility functions
│ ├── config/ # Configuration files
│ ├── validators/ # Input validation
│ ├── constants/ # Constants
│ └── app.js # Express app setup
├── tests/ # Test files
├── migrations/ # Database migrations
├── .env # Environment variables
├── .env.example # Environment template
├── package.json
├── server.js # Entry point
└── README.md
Separation of Concerns
Keep each layer focused on a specific responsibility:
Lapisan Route → Menerima permintaan HTTP
Controller → Mengorkestra logika
Lapisan Service → Berisi logika bisnis
Lapisan Data → Operasi database
Utils → Fungsi pembantu
Error Handling
Callbacks (Pola Lama)
Callback adalah pola async asli tetapi dapat menyebabkan “callback hell.”
// ❌ Callback hell
fs.readFile('file1.txt', (err, data1) => {
if (err) throw err;
fs.readFile('file2.txt', (err, data2) => {
if (err) throw err;
fs.readFile('file3.txt', (err, data3) => {
if (err) throw err;
console.log(data1, data2, data3);
});
});
});
Promises
Promise memberikan struktur lebih baik dan composability daripada callback.
// ✅ Promise-based
readFile('file1.txt')
.then(data1 => readFile('file2.txt'))
.then(data2 => readFile('file3.txt'))
.then(data3 => console.log(data1, data2, data3))
.catch(err => console.error(err));
Async/Await (Best Practice Modern)
Async/await membuat kode asynchronous terlihat dan terasa synchronous.
// ✅ Async/Await - Most readable
async function readFiles() {
try {
const data1 = await readFile('file1.txt');
const data2 = await readFile('file2.txt');
const data3 = await readFile('file3.txt');
console.log(data1, data2, data3);
} catch (error) {
console.error('Error:', error);
}
}
readFiles();
Parallel Execution dengan Async/Await
Gunakan Promise.all() untuk menjalankan beberapa operasi async secara paralel.
// Execute operation secara parallel
async function fetchUserData(userId) {
try {
// Ini execute secara parallel, bukan sequential
const [user, posts, comments] = await Promise.all([
getUser(userId),
getPosts(userId),
getComments(userId)
]);
return { user, posts, comments };
} catch (error) {
console.error('Error fetching data:', error);
}
}
Error Handling
Proper error handling mencegah cascading failure dan meningkatkan user experience.
Try/Catch dengan Async/Await
Bungkus operasi async di blok try/catch untuk menangani error secara graceful.
async function getUser(id) {
try {
const user = await User.findById(id);
if (!user) {
const error = new Error('User not found');
error.statusCode = 404;
throw error;
}
return user;
} catch (error) {
console.error('Error fetching user:', error);
throw error; // Re-throw untuk Express handle
}
}
Express Error Handling Middleware
Centralisasi penanganan error dengan middleware Express.
// Error handling middleware (must be last)
app.use((err, req, res, next) => {
console.error('Error:', err);
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
error: {
message,
status: statusCode,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
});
Kelas Error Khusus
Buat kelas error khusus untuk manajemen error yang lebih baik.
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class NotFoundError extends AppError {
constructor(message = 'Not Found') {
super(message, 404);
}
}
// Usage
if (!user) {
throw new NotFoundError('User not found');
}
Pola Middleware
Middleware functions process requests before they reach route handlers.
Request/Response Middleware
const express = require('express');
const app = express();
// Built-in middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Logging middleware
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`${timestamp} ${req.method} ${req.path}`);
next();
});
// Authentication middleware
app.use((req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
req.user = verifyToken(token);
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
// Route-specific middleware
app.get('/admin', authenticateAdmin, (req, res) => {
res.json({ message: 'Admin access' });
});
Middleware Chaining
Chain multiple middleware functions untuk process requests melalui beberapa layers.
// Middleware chain
const requestLogger = (req, res, next) => {
req.startTime = Date.now();
next();
};
const responseLogger = (req, res, next) => {
const duration = Date.now() - req.startTime;
console.log(`Response sent dalam ${duration}ms`);
next();
};
app.use(requestLogger);
app.use(responseLogger);
Environment Configuration
Manage configuration di berbagai environments menggunakan environment variables.
Menggunakan dotenv
Store sensitive configuration di .env files.
# .env
DATABASE_URL=mongodb://localhost:27017/mydb
PORT=3000
NODE_ENV=development
JWT_SECRET=super-secret-key
API_KEY=your-api-key
LOG_LEVEL=info
Configuration Module
// config.js
require('dotenv').config();
const config = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
database: {
url: process.env.DATABASE_URL,
options: {
useNewUrlParser: true,
useUnifiedTopology: true
}
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '24h'
},
isProduction: process.env.NODE_ENV === 'production',
isDevelopment: process.env.NODE_ENV === 'development'
};
// Validate required variables
if (!config.database.url) {
throw new Error('DATABASE_URL is not set');
}
module.exports = config;
Security Best Practices
Input Validation
Selalu validate dan sanitize user input.
const validator = require('validator');
// Validation middleware
const validateCreateUser = (req, res, next) => {
const { email, password, name } = req.body;
if (!email || !validator.isEmail(email)) {
return res.status(400).json({ error: 'Invalid email' });
}
if (!password || password.length < 8) {
return res.status(400).json({ error: 'Password too short' });
}
if (!name || name.length < 2) {
return res.status(400).json({ error: 'Invalid name' });
}
next();
};
app.post('/users', validateCreateUser, createUser);
Security Headers
Gunakan helmet middleware untuk set security headers.
const helmet = require('helmet');
app.use(helmet());
// This sets:
// - X-Frame-Options: DENY
// - X-Content-Type-Options: nosniff
// - X-XSS-Protection: 1; mode=block
// - Strict-Transport-Security
// - etc.
Rate Limiting
Prevent abuse dengan rate limiting middleware.
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per windowMs
message: 'Too many requests, please try again later'
});
// Apply ke semua routes
app.use(limiter);
// Atau specific routes
app.post('/login', rateLimitLogin, login);
Database Optimization
Connection Pooling
Reuse database connections untuk better performance.
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.DATABASE_URL, {
maxPoolSize: 10,
minPoolSize: 5,
socketTimeoutMS: 30000
});
console.log('Database connected');
} catch (error) {
console.error('Database connection failed:', error);
process.exit(1);
}
};
Query Optimization
Gunakan indexes dan projection untuk optimize queries.
// Add indexes ke frequently queried fields
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
// Gunakan projection untuk select only needed fields
const user = await User
.findById(id)
.select('name email') // Hanya select fields ini
.lean(); // Return plain JS objects, bukan Mongoose docs
Performance Optimization
Caching
Reduce database queries dengan caching.
const redis = require('redis');
const client = redis.createClient();
// Cache user data
async function getUser(id) {
// Check cache first
const cached = await client.get(`user:${id}`);
if (cached) {
return JSON.parse(cached);
}
// Get dari database
const user = await User.findById(id);
// Store di cache (1 hour expiry)
await client.setex(`user:${id}`, 3600, JSON.stringify(user));
return user;
}
Compression
Enable gzip compression for responses.
const compression = require('compression');
app.use(compression());
// Reduces response size by ~70% for text/JSON
Testing
Write tests to ensure code reliability.
const request = require('supertest');
const app = require('../app');
describe('User API', () => {
it('should create a user', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: '[email protected]',
password: 'password123',
name: 'Test User'
});
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
});
it('should return user by id', async () => {
const response = await request(app)
.get('/api/users/123');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('name');
});
});
Logging and Monitoring
Structured Logging
Use proper logging libraries instead of console.log.
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// Usage
logger.info('User created', { userId: user.id });
logger.error('Database error', { error: err.message });
Deployment
Process Manager (PM2)
Use PM2 to manage Node.js processes in production.
// ecosystem.config.js
module.exports = {
apps: [{
name: 'api',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production'
},
error_file: './logs/error.log',
out_file: './logs/out.log'
}]
};
Docker Deployment
Containerize your Node.js application.
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Best Practices Summary
✓ Gunakan async/await untuk asynchronous operations ✓ Implementasikan comprehensive error handling ✓ Validate semua user input ✓ Gunakan environment variables untuk configuration ✓ Implementasikan logging dan monitoring ✓ Tulis tests untuk critical functionality ✓ Gunakan middleware untuk cross-cutting concerns ✓ Optimize database queries ✓ Implementasikan caching strategies ✓ Gunakan security middleware ✓ Monitor performance metrics ✓ Gunakan containerization untuk deployment
Node.js powers millions of applications worldwide!
Last updated: January 8, 2026