engine-whatsapp memakai class statis service + controller pattern. Semua service dan controller berupa class dengan method static — tidak ada dependency injection, tidak ada instance. SocketService adalah pengecualian: singleton via getInstance().
Struktur direktori
src/
├── app.ts # Entry: Express + HTTP + Socket.IO + Mongo + restore sesi
├── config/
│ ├── config.ts # Loader env var
│ └── database.ts # Koneksi MongoDB
├── controller/ # Validasi request + bentuk response. Tanpa logika Baileys
│ ├── SessionController.ts
│ ├── MessageController.ts
│ ├── GroupController.ts
│ └── NotificationController.ts
├── middleware/
│ └── errorHandler.ts # Global error handler
├── model/ # 4 skema Mongoose
│ ├── devices.model.ts
│ ├── admin_devices.model.ts
│ ├── chatInbox.model.ts
│ └── chatHistory.model.ts
├── routes/itemRoutes.ts # SATU-SATUNYA tempat definisi route (prefix /api)
├── service/
│ ├── whatsappService.ts # ~886 baris. Inti: sesi, kirim, event listener
│ ├── incomingMessageService.ts # Pipeline pesan masuk
│ ├── socketService.ts # Singleton Socket.IO
│ ├── notificationService.ts # Relay notif + Facebook Pixel
│ ├── chatHistory/chatHistoryService.ts
│ └── devices/deviceServices.ts
└── utils/
├── helper.ts # sendWebhook, formatNumber, handleMedia, extractContent, enkripsi
└── responseHandler.ts # sendSuccess / sendErrorAlur lapisan: routes → controller → service → (model | Baileys | webhook | socket).
whatsappService.ts adalah pusat gravitasi repo. Perubahan di sana berdampak luas — baca dulu keseluruhan fungsi terkait sebelum mengedit.
Class statis — tanpa DI
Semua service dan controller memakai class dengan method static:
// controller
export class SessionController {
static async create(req: Request, res: Response) { ... }
}
// service
export class WhatsAppService {
private static sessions = new Map<string, SessionData>();
static async startSession(instanceId: string): Promise<WASocket> { ... }
}Tidak ada constructor injection, tidak ada instance. Pemanggilan langsung: WhatsAppService.startSession(instance), SessionController.create(req, res).
Pengecualian — SocketService singleton
export class SocketService {
private static instance: SocketService;
private io: SocketIOServer | null = null;
public static getInstance(): SocketService {
if (!SocketService.instance) {
SocketService.instance = new SocketService();
}
return SocketService.instance;
}
}SocketService perlu instance karena io (Socket.IO server) hanya boleh diinisialisasi sekali. Akses via SocketService.getInstance().
ES modules — tanpa ekstensi .js
package.json memakai "type": "module". Jangan menulis ekstensi .js pada import di src/ — TypeScript yang menambahkannya saat build:
import config from '../config/config'; // benar
import config from '../config/config.js'; // salah untuk sourcePath alias tidak konsisten — jangan ditiru
tsconfig.json memetakan * → src/*, sehingga import { x } from 'utils/helper' valid. Beberapa file memakai bentuk ini (MessageController.ts:4, whatsappService.ts:23), sisanya memakai relatif.
Gunakan import relatif untuk kode baru — itu bentuk mayoritas dan tidak bergantung pada resolve-tspaths.
Route — single source
src/routes/itemRoutes.ts adalah satu-satunya tempat definisi route. Route baru → daftarkan di sini, tidak di tempat lain:
const router = Router();
// Session
router.post('/new', SessionController.create);
router.get('/qrcode', SessionController.getQR);
router.get('/logout', SessionController.logout);
router.get('/device-info', SessionController.status);
// Messaging
router.post('/send-message', MessageController.sendText);
// ...
export default router;Di-mount di app.ts:23:
app.use('/api', itemRoutes);
app.use('/files', express.static(path.join(import.meta.dirname, '../public/files')));Format response — sendSuccess / sendError
Selalu lewat helper, jangan res.json() langsung:
import { sendError, sendSuccess } from '../utils/responseHandler';
sendSuccess(res, res.statusCode, data, 'Pesan sukses');
sendError(res, 400, 'Pesan error');Shape response standar:
// Sukses
{ "success": true, "message": "...", "data": { } }
// Gagal
{ "success": false, "message": "...", "error": { } } // `error` disembunyikan saat APP_ENV=productionPenanganan error di controller
Pola yang dipakai: try/catch di setiap method, lalu sendError. Perhatikan bahwa banyak controller memakai res.statusCode (bukan angka literal) sebagai status error:
// Pattern yang ada (inkonsisten — res.statusCode = 200 jika belum diset)
catch (error: any) {
return sendError(res, res.statusCode, `Gagal: ${error.message}`);
}instanceId — identitas konsisten
UUID yang mengidentifikasi satu sesi WhatsApp. Dipakai serentak sebagai:
- nama folder kredensial →
session/auth/<instanceId> - key pada
WhatsAppService.sessions(Map in-memory) - nama room Socket.IO
- kolom
iddi koleksidevices/admin_devices
Konsistensi keempatnya wajib dijaga. Jangan mengubah salah satu tanpa memperbarui yang lain.
Format nomor — formatNumber()
Selalu lewat formatNumber() di utils/helper.ts. Jangan menyusun JID manual:
export function formatNumber(number: string) {
// 1. Sudah berakhiran @s.whatsapp.net atau @g.us → biarkan
if (number.endsWith('@s.whatsapp.net') || number.endsWith('@g.us')) return number;
// 2. Mengandung '-' atau panjang > 15 digit → grup
const isGroup = number.includes('-') || number.length > 15;
if (isGroup) return number + '@g.us';
// 3. Default: personal → @s.whatsapp.net
return number.replace(/\D/g, '') + '@s.whatsapp.net';
}Deteksi grup memakai heuristik: mengandung - atau panjang > 15 digit → @g.us.
Sesi bersifat in-memory
private static sessions = new Map<string, SessionData>();Konsekuensi yang harus diingat:
- Tidak bisa PM2 cluster mode atau scaling horizontal tanpa mengubah arsitektur.
- Kredensial persisten di filesystem (
session/auth/<instanceId>), bukan di MongoDB. - Restart = semua sesi dimuat ulang dari disk (dengan jeda 2 detik antar sesi).
@lid normalisasi
WhatsApp mengirim JID @lid pada kasus tertentu. Kode menggantinya dengan remoteJidAlt. Ada di dua tempat — whatsappService.ts (messages.update) dan incomingMessageService.ts (processMessage). Bila mengubah satu, periksa yang lain:
// incomingMessageService.ts:41
if (/@lid/.test(msg.key.remoteJid!)) {
if (msg.key.remoteJidAlt) {
msg.key.remoteJid = msg.key.remoteJidAlt;
}
}
// whatsappService.ts:273 (messages.update)
if (/@lid/.test(remoteJid)) {
if (update.key.remoteJidAlt) {
update.key.remoteJid = update.key.remoteJidAlt;
}
continue;
}Event socket — pola _bubble
Beberapa event dikirim ganda dengan payload identik untuk konsumen frontend berbeda:
| Event | Pair |
|---|---|
getmessage | getmessage_bubble |
sendmessage | sendmessage_bubble |
updatechatlist | updatechatlist_bubble |
Selalu emit lewat SocketService.getInstance().emitTo(room, event, data) — jangan pakai this.io.emit() langsung (kecuali updateTeamchat yang global).
Logging — Pino
Log terstruktur via Pino, destination ./wa-logs.txt + stdout:
const mainLogger = pino({
timestamp: () => `,"time":"${new Date().toJSON()}"`,
level: config.level_log, // dari .env
}, pino.destination('./wa-logs.txt'));Child logger per sesi, tagged {session: "<instanceId>"}:
const deviceLogger = mainLogger.child({ session: instanceId });Level dari LEVEL_LOG (trace/debug/info/warn/error).
Ada juga console.log debug tercecer di beberapa file (whatsappService.ts, chatHistoryService.ts) — sebagian dikomentari. Jangan menambahkan tanpa koordinasi.
Menulis service baru
// src/service/myFeature.ts
import MyModel from '../model/myModel';
export class MyFeatureService {
static async doSomething(instanceId: string) {
return await MyModel.findOne({ id: instanceId });
}
}Daftarkan di controller atau service yang relevan. Route baru → src/routes/itemRoutes.ts.
Anti-pola yang sering muncul
res.statusCodesebagai status error — bisa 200 untuk error. Gunakan eksplisit.- Kredensial enkripsi hard-coded di
utils/helper.ts— lihat Security. - Import path alias
utils/helper— gunakan relatif untuk kode baru. console.logdebug tercecer — sebagian dikomentari, jangan tambah.- Sesi in-memory — tidak bisa cluster mode. Lihat Deployment.
Langkah berikutnya
- Service lain yang dipanggil? Baca External Services.
- Keamanan? Baca Security.
- Known issues? Baca Tech Debt.
- Cara kerja sesi? Baca Session Lifecycle.