D
Engineering

Conventions

Pola dominan adalah fat handlers — handler langsung akses MongoDB tanpa repo abstraction. Satu island clean-architecture di internal/service untuk CS rotator. Penamaan file per-tenant dengan suffix.

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.

go
// 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

text
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 envelope

Response envelope

Tidak ada standardisasi — shape berbeda per handler:

go
// 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/:

text
routes/routes.go:15-20
  ├─ repository.NewChatListRepository(db)
  ├─ repository.NewUserRepository(db)
  ├─ service.NewChatListService(chatRepo, userRepo)
  └─ handlers.NewChatListHandler(chatListService)
LayerFileTanggung jawab
Repositoryinternal/repository/chat_list_repository.goInterface + Mongo impl: FindByPhoneNumberAndInstance
Repositoryinternal/repository/user_repository.goInterface + Mongo impl: FindByPhoneNumber
Serviceinternal/service/chat_service.goGetRotator — CS assignment (least-loaded / weighted)
Handlerhandlers/chat_callback.go (ChatListHandler)CallbackMessage — pakai svc.GetRotator

Penamaan file

Handler

text
chat_<domain>.go           # default tenant
chat_<domain>_admin.go     # admin tenant
chat_<domain>_crm.go       # crm tenant

Contoh:

FileBarisDomain
chat_callback.go2943Inbound WA webhook
chat_callback_admin.go697Admin webhook
chat_callback_crm.go911CRM webhook
chat_live.go2497Inbox/list/lifecycle
chat_submit.go810Outbound send

Model

text
mongo_<collection>.go       # BSON struct untuk koleksi aktif
<name>.go                   # legacy GORM struct (dormant)

Contoh:

FileStruct
mongo_chatinbox.goChatInboxStruct
mongo_device.goDeviceStruct
mongo_device_admin.goDeviceStructAdmin (admin variant)
mongo_device_crm.goDeviceCrmStruct (crm variant)
chat_inbox.golegacy GORM (dormant)

Route

Semua POST, prefixed /v1/, snake_case:

text
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-case

Validasi

utils/utils.go:24-47 — go-playground/validator v9.31. Struct payload memakai tag validate:"required":

go
type Payload struct {
    Token      string `json:"token" validate:"required"`
    Number     string `json:"number" validate:"required"`
    Message    string `json:"message"`
    // ...
}

Error validasi return 422:

json
{
  "errors": [
    {
      "field": "token",
      "tag": "required",
      "message": "The token field is required"
    }
  ]
}

Logging

  • logger/logger.go — Zap + lumberjack (rotasi file ke logs/)
  • logger/BroadcastLogger.go — contextual logger untuk broadcast
  • Handler juga memakai log.Println ad-hoc (stdlib) — termasuk authMiddleware.go:53 yang log setiap request path

Dead code

File handler era GORM yang masih ada tapi tidak terpakai:

FileStatus
handlers/auth.goLogin pakai database.DB (dormant) — dead
handlers/contact.goGORM contact CRUD — dead
handlers/user.go24 baris, GORM — dead
handlers/label.goSemua komentar — dead
handlers/chat.goBagian sendNotif/fetch dead, struct Payload aktif

Jangan tambah dependensi ke file ini. Lihat Tech Debt.

Anti-pola yang sering muncul

  1. Query tanpa store_id — bocor antar-merchant. Lihat Multi-tenant.
  2. Hardcoded DB name DBname const — split-brain dengan config.Env.DbName. Lihat Database.
  3. Bypass auth via excluded paths — route _admin/_crm tidak butuh token. Lihat Auth & JWT.
  4. 3x duplikasi handler — perubahan default harus manual ke _admin + _crm.
  5. log.Println ad-hoc — tidak terstruktur, tidak ada level, sulit filter.
go
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:

go
v1.Post("my_endpoint", handlers.MyHandler)
// jika admin/crm:
v1.Post("my_endpoint_admin", handlers.MyHandlerAdmin)

Langkah berikutnya