Lompat ke konten
dokumentasi.id
Commit 43aad26
v1
Star1,1 rb

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-65). 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-39). ContactController.Create menambahkan satu langkah, yaitu menimpa request.UserId dengan auth.ID sebelum memanggil use case (internal/delivery/http/contact_controller.go:25-42).

MethodSumber inputNilai balikRujukan
UserController.Registerctx.BodyParser ke model.RegisterUserRequestWebResponse[*model.UserResponse]internal/delivery/http/user_controller.go:24-39
UserController.Loginctx.BodyParser ke model.LoginUserRequestWebResponse[*model.UserResponse]internal/delivery/http/user_controller.go:41-56
UserController.Currentauth.ID dari middleware.GetUserWebResponse[*model.UserResponse]internal/delivery/http/user_controller.go:58-72
UserController.Updatectx.BodyParser lalu request.ID = auth.IDWebResponse[*model.UserResponse]internal/delivery/http/user_controller.go:90-107
ContactController.Listctx.Query untuk name, email, phone dan ctx.QueryInt untuk page, sizeWebResponse[[]model.ContactResponse] plus Paginginternal/delivery/http/contact_controller.go:44-73
ContactController.Getctx.Params("contactId") dan auth.IDWebResponse[*model.ContactResponse]internal/delivery/http/contact_controller.go:75-90
AddressController.Createctx.BodyParser plus ctx.Params("contactId")WebResponse[*model.AddressResponse]internal/delivery/http/address_controller.go:24-43
AddressController.Getctx.Params("contactId") dan ctx.Params("addressId")WebResponse[*model.AddressResponse]internal/delivery/http/address_controller.go:63-81
AddressController.Deletedua param path, tanpa bodyWebResponse[bool] berisi trueinternal/delivery/http/address_controller.go:105-122

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-73).

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.
Fig. 1: Siklus satu request ber-tokenMiddleware 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-25). 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-13).

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.
Fig. 2: Keadaan di dalam middleware NewAuthToken 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-60, internal/model/user_model.go:11-13, internal/model/auth.go:3-6). Karena itu ctx.Locals("auth", auth) menyimpan satu nilai kecil, bukan entity user utuh (internal/delivery/http/middleware/auth_middleware.go:22-23).

Controller mengambil kembali nilai itu lewat GetUser, yang melakukan type assertion ctx.Locals("auth").(*model.Auth) (internal/delivery/http/middleware/auth_middleware.go:27-29). Type assertion ini akan panic bila dipanggil pada route yang tidak melewati NewAuth, jadi pemakaian GetUser hanya sah di dalam route ber-token.

⚠️ NewAuth mengembalikan fiber.ErrUnauthorized tanpa pesan tambahan, dan detail kegagalan hanya masuk ke log dengan Log.Warnf (internal/delivery/http/middleware/auth_middleware.go:16-19). 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:13).

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-20).

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.
Fig. 3: Route guest dan route ber-token menuju handler-nyaDua 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-25). Semua route lain didaftarkan setelah c.App.Use(c.AuthMiddleware), termasuk route user, contact, dan address (internal/delivery/http/route/route.go:27-44).

⚠️ Middleware dipasang di dalam SetupAuthRoute, bukan pada App secara global. Urutan pemanggilan Setup karena itu menentukan route mana yang dijaga: route guest yang didaftarkan setelah SetupAuthRoute akan ikut melewati NewAuth. 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-42), AddressController.Create juga menimpa UserId dan mengambil ContactId dari param path (internal/delivery/http/address_controller.go:24-43). Pola yang sama dipakai ContactController.Update, yang menetapkan UserId dan ID dari token serta ctx.Params("contactId") (internal/delivery/http/contact_controller.go:92-111).

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-7). Di kode controller, Paging hanya diisi oleh ContactController.List dalam bentuk model.PageMetadata (internal/model/model.go:14-19). 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-29). 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-40).