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

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

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

Guest route mendaftarkan dua endpoint user tanpa middleware apa pun. internal/delivery/http/route/route.go:22-25

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-31 internal/delivery/http/route/route.go:33-43

MethodPathHandlerMiddlewareBaris route.go
POST/api/usersUserController.Register—23
POST/api/users/_loginUserController.Login—24
DELETE/api/usersUserController.LogoutAuthMiddleware29
PATCH/api/users/_currentUserController.UpdateAuthMiddleware30
GET/api/users/_currentUserController.CurrentAuthMiddleware31
GET/api/contactsContactController.ListAuthMiddleware33
POST/api/contacts/:contactId/addressesAddressController.CreateAuthMiddleware40

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-29

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.
Fig. 1: Perjalanan request POST /api/usersRequest 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-39

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-117

Response terakhir ditulis sebagai model.WebResponse[*model.UserResponse] dengan field Data. internal/delivery/http/user_controller.go:38 Wrapper itu juga menyediakan Paging dan Errors yang keduanya ber-omitempty. internal/model/model.go:3-7

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-64 internal/repository/repository.go:9-29

UrutanPemanggilan di CreateError yang dikembalikan
1c.Validate.Struct(request)fiber.ErrBadRequest
2UserRepository.CountById(tx, request.ID)fiber.ErrInternalServerError
3cabang total > 0fiber.ErrConflict
4bcrypt.GenerateFromPasswordfiber.ErrInternalServerError
5UserRepository.Create(tx, user)fiber.ErrInternalServerError
6tx.Commit().Errorfiber.ErrInternalServerError
7UserProducer.Send(event) bila producer aktiffiber.ErrInternalServerError
8converter.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-64

⚠️ UserProducer.Send dipanggil setelah tx.Commit(), bukan sebelum. Ketika publish event gagal, usecase mengembalikan fiber.ErrInternalServerError padahal baris user sudah tersimpan di database. internal/usecase/user_usecase.go:100-116

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.
Fig. 2: Jalur request yang melewati auth middlewareMiddleware 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-25

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-60 Query token dijalankan lewat Where("token = ?", token).First(user). internal/repository/user_repository.go:21-23

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-72 internal/usecase/user_usecase.go:164-185

⚠️ GetUser melakukan type assertion tanpa pengecekan: ctx.Locals("auth").(*model.Auth). Handler yang memanggilnya wajib terdaftar setelah c.App.Use(c.AuthMiddleware); memindahkannya ke SetupGuestRoute membuat request panic, bukan menghasilkan 401. internal/delivery/http/middleware/auth_middleware.go:27-29 internal/delivery/http/route/route.go:27-31

Bentuk response dan jalur gagal

StructFieldKey JSON
WebResponseDatadata
WebResponsePagingpaging,omitempty
WebResponseErrorserrors,omitempty
UserResponseIDid,omitempty
UserResponseNamename,omitempty
UserResponseTokentoken,omitempty
UserResponseCreatedAtcreated_at,omitempty
UserResponseUpdatedAtupdated_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-9 internal/model/converter/user_converter.go:8-21

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.
Fig. 3: Penanganan error dari controller ke response JSONError 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-28

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.