2.1
Alur Request HTTP
Halaman ini menelusuri satu request HTTP dari route Fiber sampai response JSON. Contoh utamanya POST /api/users, karena jalur itu melewati semua lapisan: controller, usecase, repository, lalu converter. Perakitan objeknya dibahas di Konfigurasi dan Bootstrap, sedangkan pembagian tanggung jawab tiap lapisan ada di Arsitektur Berlapis.
Dua kelompok rute
RouteConfig.Setup hanya memanggil dua method: SetupGuestRoute lebih dulu, lalu SetupAuthRoute. Urutan itu menentukan middleware mana yang menempel pada handler mana. internal/delivery/http/route/route.go:17-201
Guest route mendaftarkan dua endpoint user tanpa middleware apa pun. internal/delivery/http/route/route.go:22-252
SetupAuthRoute memasang c.App.Use(c.AuthMiddleware) sebelum mendaftarkan rute user yang butuh token, lalu seluruh rute kontak dan address. internal/delivery/http/route/route.go:27-313 internal/delivery/http/route/route.go:33-434
| Method | Path | Handler | Middleware | Baris route.go |
|---|---|---|---|---|
POST | /api/users | UserController.Register | — | 23 |
POST | /api/users/_login | UserController.Login | — | 24 |
DELETE | /api/users | UserController.Logout | AuthMiddleware | 29 |
PATCH | /api/users/_current | UserController.Update | AuthMiddleware | 30 |
GET | /api/users/_current | UserController.Current | AuthMiddleware | 31 |
GET | /api/contacts | ContactController.List | AuthMiddleware | 33 |
POST | /api/contacts/:contactId/addresses | AddressController.Create | AuthMiddleware | 40 |
Sepuluh rute terakhir di kelompok auth mencakup lima rute kontak dan lima rute address, semuanya berjalan di belakang AuthMiddleware. Handler di kelompok itu membaca identitas pemanggil lewat middleware.GetUser(ctx), yang mengambil ctx.Locals("auth"). Handler guest tidak pernah memanggilnya. internal/delivery/http/middleware/auth_middleware.go:27-295
Perjalanan POST /api/users
sequenceDiagram
autonumber
participant C as Client
participant RT as RouteConfig.SetupGuestRoute
participant U as UserController.Register
participant UC as UserUseCase.Create
participant DB as gorm.DB (tx)
participant R as UserRepository
participant CV as converter.UserToResponse
C->>RT: POST /api/users
RT->>U: Register(ctx)
U->>U: ctx.BodyParser(RegisterUserRequest)
Note over U: parse gagal, kembalikan fiber.ErrBadRequest
U->>UC: Create(ctx.UserContext(), request)
rect rgba(51,103,145,0.35)
UC->>DB: Begin()
UC->>UC: Validate.Struct(request)
UC->>R: CountById(tx, request.ID)
R->>DB: Where id = ? Count
UC->>R: Create(tx, user)
R->>DB: Create(entity)
UC->>DB: Commit()
end
UC->>CV: UserToResponse(user)
CV-->>UC: model.UserResponse
UC-->>U: response
U-->>C: ctx.JSON(WebResponse)
%% fig: Perjalanan request POST /api/users
%% desc: Request masuk lewat guest route, controller mem-parse body, usecase membuka satu transaksi gorm.DB dan memanggil repository, lalu converter membentuk response JSON.UserController.Register membuat model.RegisterUserRequest kosong, mengisinya dengan ctx.BodyParser, lalu memanggil c.UseCase.Create(ctx.UserContext(), request). Bila parse gagal, ia menulis log Failed to parse request body : %+v dan mengembalikan fiber.ErrBadRequest. Bila usecase mengembalikan error, controller menulis Failed to register user : %+v lalu meneruskan error yang sama tanpa membungkusnya. internal/delivery/http/user_controller.go:24-396
Di sisi usecase, Create memvalidasi request, menanyakan jumlah baris lewat CountById untuk menolak ID yang sudah terpakai, meng-hash password dengan bcrypt.GenerateFromPassword(..., bcrypt.DefaultCost), menyimpan entity lewat UserRepository.Create, dan meng-commit transaksi. Bila producer Kafka aktif, event dikirim setelah commit; bila c.UserProducer bernilai nil, langkah itu dilewati dengan log Kafka producer is disabled, skipping user created event. internal/usecase/user_usecase.go:62-1177
Response terakhir ditulis sebagai model.WebResponse[*model.UserResponse] dengan field Data. internal/delivery/http/user_controller.go:388 Wrapper itu juga menyediakan Paging dan Errors yang keduanya ber-omitempty. internal/model/model.go:3-79
Transaksi gorm.DB di dalam usecase
Setiap method usecase membuka transaksi sendiri dengan tx := c.DB.WithContext(ctx).Begin() dan mendaftarkan defer tx.Rollback(), lalu meneruskan handle tx ke repository. Repository tidak pernah membuka transaksi sendiri: ia hanya menerima parameter db *gorm.DB. internal/usecase/user_usecase.go:63-6410 internal/repository/repository.go:9-2911
| Urutan | Pemanggilan di Create | Error yang dikembalikan |
|---|---|---|
| 1 | c.Validate.Struct(request) | fiber.ErrBadRequest |
| 2 | UserRepository.CountById(tx, request.ID) | fiber.ErrInternalServerError |
| 3 | cabang total > 0 | fiber.ErrConflict |
| 4 | bcrypt.GenerateFromPassword | fiber.ErrInternalServerError |
| 5 | UserRepository.Create(tx, user) | fiber.ErrInternalServerError |
| 6 | tx.Commit().Error | fiber.ErrInternalServerError |
| 7 | UserProducer.Send(event) bila producer aktif | fiber.ErrInternalServerError |
| 8 | converter.UserToResponse(user) | — |
Karena defer tx.Rollback() selalu terdaftar, setiap return sebelum commit membatalkan perubahan yang sudah ditulis di dalam transaksi. Pemanggilan Rollback setelah Commit berhasil tidak memberi efek tambahan. internal/usecase/user_usecase.go:63-6410
⚠️
UserProducer.Senddipanggil setelahtx.Commit(), bukan sebelum. Ketika publish event gagal, usecase mengembalikanfiber.ErrInternalServerErrorpadahal baris user sudah tersimpan di database. internal/usecase/user_usecase.go:100-11612
Jalur yang memakai token: GET /api/users/_current
graph LR
REQ["GET /api/users/_current"] --> MW["middleware.NewAuth"]
MW --> VF["UserUseCase.Verify"]
VF --> FBT["UserRepository.FindByToken"]
FBT --> DB[("tabel users")]
VF --> AUTHR["model.Auth { ID }"]
AUTHR --> LOC["ctx.Locals 'auth'"]
LOC --> GU["middleware.GetUser"]
GU --> CU["UserController.Current"]
CU --> UCC["UserUseCase.Current"]
UCC --> FBI["UserRepository.FindById"]
FBI --> DB
UCC --> CVT["converter.UserToResponse"]
CVT --> JSON["WebResponse UserResponse"]
MW -.->|"error"| DENY["fiber.ErrUnauthorized"]
style MW fill:#f59e0b,color:#000
style DB fill:#336791,color:#fff
style VF fill:#f59e0b,color:#000
style CVT fill:#8b5cf6,color:#fff
style JSON fill:#2d7d46,color:#fff
style DENY fill:#dc2626,color:#fff
%% fig: Jalur request yang melewati auth middleware
%% desc: Middleware memverifikasi token lewat usecase dan repository, menyimpan hasilnya di ctx.Locals, lalu controller membaca identitas itu untuk mengambil user.middleware.NewAuth membangun model.VerifyUserRequest dari header Authorization dengan nilai default NOT_FOUND, memanggil Verify, lalu menyimpan hasilnya di ctx.Locals("auth") sebelum ctx.Next(). Setiap error dari Verify ditelan dan diganti fiber.ErrUnauthorized. internal/delivery/http/middleware/auth_middleware.go:10-2513
Verify sendiri membuka transaksi, memvalidasi request, memanggil UserRepository.FindByToken, lalu meng-commit dan mengembalikan model.Auth{ID: user.ID}. Token yang tidak ditemukan menghasilkan fiber.ErrNotFound. internal/usecase/user_usecase.go:38-6014 Query token dijalankan lewat Where("token = ?", token).First(user). internal/repository/user_repository.go:21-2315
Di handler, Current mengambil middleware.GetUser(ctx), menyusun model.GetUserRequest dari auth.ID, memanggil c.UseCase.Current, lalu menulis WebResponse. Usecase-nya memakai FindById dan mengembalikan converter.UserToResponse(user). internal/delivery/http/user_controller.go:58-7216 internal/usecase/user_usecase.go:164-18517
⚠️
GetUsermelakukan type assertion tanpa pengecekan:ctx.Locals("auth").(*model.Auth). Handler yang memanggilnya wajib terdaftar setelahc.App.Use(c.AuthMiddleware); memindahkannya keSetupGuestRoutemembuat request panic, bukan menghasilkan 401. internal/delivery/http/middleware/auth_middleware.go:27-295 internal/delivery/http/route/route.go:27-313
Bentuk response dan jalur gagal
| Struct | Field | Key JSON |
|---|---|---|
WebResponse | Data | data |
WebResponse | Paging | paging,omitempty |
WebResponse | Errors | errors,omitempty |
UserResponse | ID | id,omitempty |
UserResponse | Name | name,omitempty |
UserResponse | Token | token,omitempty |
UserResponse | CreatedAt | created_at,omitempty |
UserResponse | UpdatedAt | updated_at,omitempty |
Field UserResponse didefinisikan lengkap, tetapi tidak semua terisi. UserToResponse mengisi ID, Name, CreatedAt, dan UpdatedAt, sedangkan UserToTokenResponse hanya mengisi Token untuk response login. Karena semuanya ber-omitempty, field yang kosong hilang dari JSON. internal/model/user_model.go:3-918 internal/model/converter/user_converter.go:8-2119
flowchart TD
ERR["fiber.ErrBadRequest / ErrNotFound / ErrConflict / ErrUnauthorized"] --> RET["handler: return err"]
RET --> EH["config.NewErrorHandler"]
EH --> CHK{"err bertipe fiber.Error"}
CHK -->|ya| CODE["code = e.Code"]
CHK -->|tidak| FIVE["code = 500"]
CODE --> OUT["ctx.Status(code).JSON"]
FIVE --> OUT
OUT --> BODY["errors: err.Error()"]
style ERR fill:#dc2626,color:#fff
style RET fill:#dc2626,color:#fff
style EH fill:#f59e0b,color:#000
style CHK fill:#f59e0b,color:#000
style OUT fill:#2d7d46,color:#fff
%% fig: Penanganan error dari controller ke response JSON
%% desc: Error yang dikembalikan handler masuk ke ErrorHandler Fiber, status diambil dari fiber.Error atau jatuh ke 500, dan body hanya berisi key errors.Controller tidak menulis body error sendiri; ia mengembalikan nilai seperti fiber.ErrBadRequest dan fiber.ErrConflict. config.NewErrorHandler, yang dipasang pada fiber.Config, memeriksa apakah error bertipe *fiber.Error, memakai e.Code bila ya, memakai 500 bila tidak, lalu menulis ctx.Status(code).JSON(fiber.Map{"errors": err.Error()}). internal/config/fiber.go:18-2820
Dengan pola ini kegagalan satu request tidak pernah sampai ke lapisan HTTP sebagai panic: seluruh error per lapisan diterjemahkan menjadi *fiber.Error di usecase, lalu diformat di satu tempat. Jalur event Kafka setelah commit dibahas terpisah di Alur Event Kafka, dan detail middleware ada di Delivery HTTP dan Middleware.