Checklist Wajib Sebelum Melepas REST API ke Production
Kenapa Checklist Ini Penting?
Saya pernah melepas API ke production tanpa checklist. Hasilnya? Incident tengah malam: database kena brute force, response time melonjak 5x karena tidak ada rate limit, dan leak API key di commit history Git. Sejak saat itu, saya buat checklist ini dan jalankan setiap rilis.
1. Security: Autentikasi & Authorization
JWT Configuration yang Benar
// Node.js - Best practice JWT
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{
expiresIn: '15m', // Access token pendek
algorithm: 'HS256', // Explicit
issuer: 'myapi.com',
audience: user.role,
}
);
// Refresh token terpisah, longer-lived
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d', algorithm: 'HS256' }
);
✅ Checklist:
- Token kadaluarsa < 1 jam (access token)
- Refresh token mekanisme tersedia
- Algoritma explicit (
HS256/RS256), tidak bolehnone - Secret key panjang minimal 32 karakter
- Validasi
iss(issuer) danaud(audience)
Password Hashing
const bcrypt = require('bcrypt');
// ⚠️ JANGAN pernah simpan plain text!
const hashedPassword = await bcrypt.hash(password, 12); // cost factor 12
2. Rate Limiting & Throttling
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 menit
max: 100, // 100 request per window per IP
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
error: { code: 429, message: 'Too many requests' }
}
});
// Limit lebih ketat untuk endpoint sensitif
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 jam
max: 5, // 5 percobaan login per jam per IP
});
app.use('/api/', apiLimiter);
app.use('/auth/login', authLimiter);
✅ Checklist:
- Global rate limit pasang (~100 req/15min)
- Endpoint login: max 5-10 percobaan per jam
- Endpoint reset password: max 3 per jam
- Beri header
RateLimit-*untuk client awareness
3. HTTP Security Headers
const helmet = require('helmet');
app.use(helmet());
// Atau konfigurasi spesifik
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'trusted.cdn.com'"],
}
},
crossOriginEmbedderPolicy: false,
}));
Header yang harus ada di response:
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0 // Modern, rely on CSP
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Referrer-Policy: no-referrer-when-downgrade
4. CORS Policy yang Aman
const cors = require('cors');
app.use(cors({
origin: (origin, callback) => {
const whitelist = ['https://app.example.com', 'https://admin.example.com'];
if (!origin || whitelist.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
maxAge: 86400 // Cache preflight 24 jam
}));
⚠️ Jangan: origin: '*' jika credentials: true. Browser menolak dan security vulnerability!
5. Environment Variables & Secrets Management
File .env.example
# Database
DATABASE_URL=postgresql://user:pass@host:5432/dbname
# JWT Secrets (MIN 32 char)
JWT_SECRET=use-openssl-rand-hex-32-to-generate
REFRESH_TOKEN_SECRET=another-32-char-random-string
# API Rate Limit
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
# Service keys
STRIPE_SECRET_KEY=sk_test_xxxxxxx
REDIS_URL=redis://localhost:6379
# Environment
NODE_ENV=production
PORT=3000
Generate Secret yang Kuat
# Generate dengan OpenSSL
openssl rand -hex 32
✅ Checklist:
-
.envmasuk.gitignore - Tidak ada secret hardcode di source code
- Rotasi secret tiap 3-6 bulan
- Gunakan Vault (AWS Secrets Manager, Doppler) untuk production
- Scan repo:
gitleaksataugit-secrets
6. Input Validation
const { body, validationResult } = require('express-validator');
app.post('/users',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 12 }).withMessage('Password minimal 12 karakter'),
body('name').trim().isLength({ min: 2, max: 100 }),
body('age').isInt({ min: 18, max: 120 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
// ... handle valid input
}
);
7. Logging & Monitoring
const morgan = require('morgan');
app.use(morgan('combined', { stream: winston.stream }));
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: Date.now() - start,
ip: req.ip,
userAgent: req.get('User-Agent'),
});
});
next();
});
✅ Checklist:
- Logging terstruktur (JSON, bukan plain text)
- JANGAN log informasi sensitif (password, token, PII)
- Health check endpoint:
GET /healthreturn 200 OK - Metrics endpoint untuk Prometheus
- Alerting otomatis (Sentry, Datadog, atau Prometheus AlertManager)
8. Database Indexing
-- Index untuk foreign key
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_orders_status ON orders(status, created_at);
✅ Checklist:
- Explain plan untuk query paling sering
- Composite index untuk
WHERE ... ORDER BY - Diagonal index untuk query range (
created_at BETWEEN ...)
Kesimpulan
Checklist ini bukan opsional — itu standar minimum. Setelah Anda memublikasikan API di production, attacker otomatis menemukan endpoint Anda. Tanpa helm, tanpa rate limit, tanpa CORS yang tepat — API Anda open season untuk abuse.
Print checklist ini, tempel di meja Anda, dan ceklis sebelum setiap rilis.