backend-go memakai dua pola arsitektur yang berjalan bersamaan: “fat handlers” (dominan) dan satu “island” clean-architecture di internal/service/.
Pola dominan — fat handlers
Handler langsung melakukan semua hal: parse body, validasi, buka koleksi MongoDB, query, dan return JSON. Tidak ada repository abstraction.
// handlers/chat_submit.go:26-75 — pola tipikal
func SubmitMessage(c *fiber.Ctx) error {
payload := new(Payload)
c.BodyParser(payload)
if errors := utils.Validate(payload); len(errors) > 0 {
return utils.ValidationResponse(c, errors)
}
if payload.Message == "" {
return c.JSON(fiber.Map{
"result": false,
"message": "Pesan harus diisi",
})
}
// akses MongoDB langsung
filterGetDevice := bson.M{"token": payload.Token}
orderPaket, err := GetDevicebyFilter(filterGetDevice)
if err != nil {
return c.JSON(fiber.Map{"result": false, "message": err.Error()})
}
errMessage := validateAuthMongo(payload, *orderPaket)
// ...
return c.JSON(fiber.Map{
"result": true,
"message": "Kirim pesan sukses!",
"status": "pending",
"data": chatInbox,
})
}Pola ini berulang ~50+ kali di handlers/.
Struktur handler tipikal
1. c.BodyParser(payload) — parse JSON body
2. utils.Validate(payload) — validasi tag validator
3. Validasi bisnis manual (if) — field wajib, logika
4. Ambil device/settings dari MongoDB — bson.M filter
5. Validasi auth device — validateAuthMongo
6. Operasi utama (save/query) — InsertOne/FindOne/Aggregate
7. Return c.JSON(fiber.Map{...}) — response envelopeResponse envelope
Tidak ada standardisasi — shape berbeda per handler:
// Sukses
return c.JSON(fiber.Map{
"result": true,
"message": "Kirim pesan sukses!",
"data": chatInbox,
})
// Error bisnis
return c.JSON(fiber.Map{
"result": false,
"message": err.Error(),
})
// Error validasi (422)
return utils.ValidationResponse(c, errors) // {"errors": [...]}Pola clean-architecture island
Satu-satunya handler yang memakai layering ada di internal/:
routes/routes.go:15-20
├─ repository.NewChatListRepository(db)
├─ repository.NewUserRepository(db)
├─ service.NewChatListService(chatRepo, userRepo)
└─ handlers.NewChatListHandler(chatListService)| Layer | File | Tanggung jawab |
|---|---|---|
| Repository | internal/repository/chat_list_repository.go | Interface + Mongo impl: FindByPhoneNumberAndInstance |
| Repository | internal/repository/user_repository.go | Interface + Mongo impl: FindByPhoneNumber |
| Service | internal/service/chat_service.go | GetRotator — CS assignment (least-loaded / weighted) |
| Handler | handlers/chat_callback.go (ChatListHandler) | CallbackMessage — pakai svc.GetRotator |
Penamaan file
Handler
chat_<domain>.go # default tenant
chat_<domain>_admin.go # admin tenant
chat_<domain>_crm.go # crm tenantContoh:
| File | Baris | Domain |
|---|---|---|
chat_callback.go | 2943 | Inbound WA webhook |
chat_callback_admin.go | 697 | Admin webhook |
chat_callback_crm.go | 911 | CRM webhook |
chat_live.go | 2497 | Inbox/list/lifecycle |
chat_submit.go | 810 | Outbound send |
Model
mongo_<collection>.go # BSON struct untuk koleksi aktif
<name>.go # legacy GORM struct (dormant)Contoh:
| File | Struct |
|---|---|
mongo_chatinbox.go | ChatInboxStruct |
mongo_device.go | DeviceStruct |
mongo_device_admin.go | DeviceStructAdmin (admin variant) |
mongo_device_crm.go | DeviceCrmStruct (crm variant) |
chat_inbox.go | legacy GORM (dormant) |
Route
Semua POST, prefixed /v1/, snake_case:
POST /v1/chat_inbox
POST /v1/chat_inbox_admin
POST /v1/send_message
POST /v1/broadcast/scheduler # pengecualian: slash untuk nested
POST /v1/chats/request-escalate # pengecualian: slash + kebab-caseValidasi
utils/utils.go:24-47 — go-playground/validator v9.31. Struct payload memakai tag validate:"required":
type Payload struct {
Token string `json:"token" validate:"required"`
Number string `json:"number" validate:"required"`
Message string `json:"message"`
// ...
}Error validasi return 422:
{
"errors": [
{
"field": "token",
"tag": "required",
"message": "The token field is required"
}
]
}Logging
logger/logger.go— Zap + lumberjack (rotasi file kelogs/)logger/BroadcastLogger.go— contextual logger untuk broadcast- Handler juga memakai
log.Printlnad-hoc (stdlib) — termasukauthMiddleware.go:53yang log setiap request path
Dead code
File handler era GORM yang masih ada tapi tidak terpakai:
| File | Status |
|---|---|
handlers/auth.go | Login pakai database.DB (dormant) — dead |
handlers/contact.go | GORM contact CRUD — dead |
handlers/user.go | 24 baris, GORM — dead |
handlers/label.go | Semua komentar — dead |
handlers/chat.go | Bagian sendNotif/fetch dead, struct Payload aktif |
Jangan tambah dependensi ke file ini. Lihat Tech Debt.
Anti-pola yang sering muncul
- Query tanpa
store_id— bocor antar-merchant. Lihat Multi-tenant. - Hardcoded DB name
DBnameconst — split-brain denganconfig.Env.DbName. Lihat Database. - Bypass auth via excluded paths — route
_admin/_crmtidak butuh token. Lihat Auth & JWT. - 3x duplikasi handler — perubahan
defaultharus manual ke_admin+_crm. log.Printlnad-hoc — tidak terstruktur, tidak ada level, sulit filter.
Menulis handler baru
func MyHandler(c *fiber.Ctx) error {
payload := new(MyPayload)
if err := c.BodyParser(payload); err != nil {
return c.JSON(fiber.Map{"result": false, "message": err.Error()})
}
if errs := utils.Validate(payload); len(errs) > 0 {
return utils.ValidationResponse(c, errs)
}
col := mongo.MongoClient.Database(DBname).Collection("my_collection")
filter := bson.M{"store_id": payload.StoreId, "id": payload.Id}
// ... query
return c.JSON(fiber.Map{"result": true, "data": result})
}Daftar di routes/routes.go:
v1.Post("my_endpoint", handlers.MyHandler)
// jika admin/crm:
v1.Post("my_endpoint_admin", handlers.MyHandlerAdmin)Langkah berikutnya
- Service lain yang dipanggil? Baca External Services.
- Known issues? Baca Tech Debt.