8
Delivery HTTP dan Middleware
internal/delivery/http adalah lapisan terluar aplikasi. Tiga controller menangani HTTP, satu middleware menjaga route ber-token, dan RouteConfig menyatukan keduanya. Perakitan seluruh objek terjadi di Bootstrap, yang membuat controller, middleware, lalu memanggil routeConfig.Setup() sebagai langkah terakhir (internal/config/app.go:50-651). Gambaran umum perjalanan satu request lintas lapisan ada di Alur Request HTTP; halaman ini fokus pada kode delivery-nya.
Controller: parsing input, memanggil use case, membungkus WebResponse
Semua method controller mengikuti pola yang sama. Ambil identitas pemanggil lewat middleware.GetUser, parsing input dari fiber.Ctx, panggil use case dengan ctx.UserContext(), lalu bungkus hasilnya ke model.WebResponse. UserController.Register menunjukkan pola paling ringkas: ctx.BodyParser yang gagal langsung menjadi fiber.ErrBadRequest, sedangkan error dari use case diteruskan apa adanya (internal/delivery/http/user_controller.go:24-392). ContactController.Create menambahkan satu langkah, yaitu menimpa request.UserId dengan auth.ID sebelum memanggil use case (internal/delivery/http/contact_controller.go:25-423).
| Method | Sumber input | Nilai balik | Rujukan |
|---|---|---|---|
UserController.Register | ctx.BodyParser ke model.RegisterUserRequest | WebResponse[*model.UserResponse] | internal/delivery/http/user_controller.go:24-392 |
UserController.Login | ctx.BodyParser ke model.LoginUserRequest | WebResponse[*model.UserResponse] | internal/delivery/http/user_controller.go:41-564 |
UserController.Current | auth.ID dari middleware.GetUser | WebResponse[*model.UserResponse] | internal/delivery/http/user_controller.go:58-725 |
UserController.Update | ctx.BodyParser lalu request.ID = auth.ID | WebResponse[*model.UserResponse] | internal/delivery/http/user_controller.go:90-1076 |
ContactController.List | ctx.Query untuk name, email, phone dan ctx.QueryInt untuk page, size | WebResponse[[]model.ContactResponse] plus Paging | internal/delivery/http/contact_controller.go:44-737 |
ContactController.Get | ctx.Params("contactId") dan auth.ID | WebResponse[*model.ContactResponse] | internal/delivery/http/contact_controller.go:75-908 |
AddressController.Create | ctx.BodyParser plus ctx.Params("contactId") | WebResponse[*model.AddressResponse] | internal/delivery/http/address_controller.go:24-439 |
AddressController.Get | ctx.Params("contactId") dan ctx.Params("addressId") | WebResponse[*model.AddressResponse] | internal/delivery/http/address_controller.go:63-8110 |
AddressController.Delete | dua param path, tanpa body | WebResponse[bool] berisi true | internal/delivery/http/address_controller.go:105-12211 |
Hanya ContactController.List yang mengisi Paging. Nilai TotalPage dihitung dengan math.Ceil dari TotalItem dibagi Size, dan angka Page serta Size diambil dari query yang sudah dibatasi default 1 dan 10 (internal/delivery/http/contact_controller.go:44-737).
Satu request ber-token melewati middleware, controller, use case, dan berakhir di error handler. Urutan itu digambarkan berikut.
sequenceDiagram
participant C as Client
participant M as NewAuth
participant Ctrl as ContactController
participant UC as ContactUseCase
participant EH as NewErrorHandler
C->>M: request dengan header Authorization
M->>UC: Verify dengan VerifyUserRequest
UC-->>M: model.Auth berisi ID
M->>M: ctx.Locals auth diisi
M->>Ctrl: ctx.Next
Ctrl->>Ctrl: BodyParser CreateContactRequest
Ctrl->>UC: Create dengan ctx.UserContext
alt use case sukses
UC-->>Ctrl: ContactResponse
Ctrl-->>C: WebResponse berisi data
else use case gagal
UC-->>Ctrl: error
Ctrl-->>EH: return err
EH-->>C: JSON berisi errors
end
%% fig: Siklus satu request ber-token
%% desc: Middleware mengisi ctx.Locals sebelum controller mem-parsing body, dan error dari use case berakhir di NewErrorHandler.Middleware NewAuth: token menjadi ctx.Locals
NewAuth adalah fungsi yang menerima *usecase.UserUseCase dan mengembalikan fiber.Handler (internal/delivery/http/middleware/auth_middleware.go:10-2512). Token dibaca dari header Authorization dengan nilai default NOT_FOUND, sehingga request tanpa header tetap menjadi request yang divalidasi, bukan ditolak lebih awal di level middleware (internal/delivery/http/middleware/auth_middleware.go:12-1313).
stateDiagram-v2
[*] --> BacaHeader
BacaHeader --> Verifikasi: token dari Authorization atau NOT_FOUND
Verifikasi --> SimpanLocals: Verify mengembalikan model.Auth
Verifikasi --> Ditolak: Verify mengembalikan error
SimpanLocals --> Lolos: ctx.Locals auth lalu ctx.Next
Ditolak --> [*]: return fiber.ErrUnauthorized
Lolos --> [*]
style BacaHeader fill:#8b5cf6,color:#fff
style Verifikasi fill:#f59e0b,color:#000
style SimpanLocals fill:#2d7d46,color:#fff
style Lolos fill:#2d7d46,color:#fff
style Ditolak fill:#dc2626,color:#fff
%% fig: Keadaan di dalam middleware NewAuth
%% desc: Token dari header divalidasi lewat Verify, lalu identitas disimpan di ctx.Locals atau request ditolak dengan ErrUnauthorized.Verifikasi sepenuhnya diserahkan ke UserUseCase.Verify. Method itu memvalidasi VerifyUserRequest yang mensyaratkan Token tidak kosong dengan batas max=100, memanggil UserRepository.FindByToken, lalu mengembalikan model.Auth yang hanya memuat ID (internal/usecase/user_usecase.go:38-6014, internal/model/user_model.go:11-1315, internal/model/auth.go:3-616). Karena itu ctx.Locals("auth", auth) menyimpan satu nilai kecil, bukan entity user utuh (internal/delivery/http/middleware/auth_middleware.go:22-2317).
Controller mengambil kembali nilai itu lewat GetUser, yang melakukan type assertion ctx.Locals("auth").(*model.Auth) (internal/delivery/http/middleware/auth_middleware.go:27-2918). Type assertion ini akan panic bila dipanggil pada route yang tidak melewati NewAuth, jadi pemakaian GetUser hanya sah di dalam route ber-token.
⚠️
NewAuthmengembalikanfiber.ErrUnauthorizedtanpa pesan tambahan, dan detail kegagalan hanya masuk ke log denganLog.Warnf(internal/delivery/http/middleware/auth_middleware.go:16-1919). Jangan menambahkan detail token ke response; token yang dikirim client akan ikut tercatat bila level log diturunkan ke debug (internal/delivery/http/middleware/auth_middleware.go:1320).
Peta route guest dan route ber-token
RouteConfig hanya menyimpan empat field: App, tiga controller, dan AuthMiddleware. Setup memanggil SetupGuestRoute lebih dulu, baru SetupAuthRoute (internal/delivery/http/route/route.go:9-2021).
graph LR
G1["POST /api/users"] --> H1["UserController.Register"]
G2["POST /api/users/_login"] --> H2["UserController.Login"]
MW{"c.App.Use AuthMiddleware"} --> B1["DELETE /api/users"]
MW --> B2["PATCH dan GET /api/users/_current"]
MW --> B3["/api/contacts dan /api/contacts/:contactId"]
MW --> B4["/api/contacts/:contactId/addresses"]
B1 --> H3["UserController.Logout"]
B2 --> H4["UserController.Current dan Update"]
B3 --> H5["ContactController"]
B4 --> H6["AddressController"]
H1 --> U["UserUseCase"]
H2 --> U
H3 --> U
H4 --> U
H5 --> CU["ContactUseCase"]
H6 --> AU["AddressUseCase"]
style G1 fill:#2d7d46,color:#fff
style G2 fill:#2d7d46,color:#fff
style MW fill:#f59e0b,color:#000
style U fill:#8b5cf6,color:#fff
style CU fill:#8b5cf6,color:#fff
style AU fill:#8b5cf6,color:#fff
%% fig: Route guest dan route ber-token menuju handler-nya
%% desc: Dua route guest didaftarkan tanpa middleware, sedangkan seluruh route lain berada setelah c.App.Use AuthMiddleware dan berakhir di use case masing-masing.Route guest hanya dua, yaitu Register dan Login (internal/delivery/http/route/route.go:22-2522). Semua route lain didaftarkan setelah c.App.Use(c.AuthMiddleware), termasuk route user, contact, dan address (internal/delivery/http/route/route.go:27-4423).
⚠️ Middleware dipasang di dalam
SetupAuthRoute, bukan padaAppsecara global. Urutan pemanggilanSetupkarena itu menentukan route mana yang dijaga: route guest yang didaftarkan setelahSetupAuthRouteakan ikut melewatiNewAuth. Daftar endpoint lengkap beserta method-nya ada di API Spec dan Daftar Endpoint.
Kepemilikan data selalu diambil dari token
Controller contact dan address tidak pernah memakai UserId dari body. ContactController.Create menimpa request.UserId dengan auth.ID (internal/delivery/http/contact_controller.go:25-423), AddressController.Create juga menimpa UserId dan mengambil ContactId dari param path (internal/delivery/http/address_controller.go:24-439). Pola yang sama dipakai ContactController.Update, yang menetapkan UserId dan ID dari token serta ctx.Params("contactId") (internal/delivery/http/contact_controller.go:92-11124).
Akibatnya UserId di body request tidak berpengaruh, meskipun field itu ada di struct request. Pemeriksaan kepemilikan sebenarnya baru terjadi di lapisan use case, ketika filter user_id dipakai saat query; mekanismenya dibahas di Lapisan Usecase, Model, dan Converter.
Bentuk respons dan penanganan error
Setiap controller mengembalikan model.WebResponse[T] yang berisi Data, Paging, dan Errors (internal/model/model.go:3-725). Di kode controller, Paging hanya diisi oleh ContactController.List dalam bentuk model.PageMetadata (internal/model/model.go:14-1926). Field Errors tidak diisi controller sama sekali.
Isi errors berasal dari NewErrorHandler yang dipasang pada fiber.Config. Handler itu mengambil code dari *fiber.Error bila error bertipe tersebut, lalu membalas JSON {"errors": err.Error()} (internal/config/fiber.go:18-2927). Karena controller mengembalikan error use case apa adanya, pesan error yang muncul di response adalah pesan dari fiber.Error, bukan pesan internal seperti Failed to create address yang hanya masuk log (internal/delivery/http/address_controller.go:36-4028).