233. Arsitektur Clean RESTful API (Entity, Repository, Service, Controller, Router)
Memahami pemisahan layer pada arsitektur standar produksi aplikasi Go REST API.
Layered Architecture RESTful API
Untuk membangun REST API yang siap dipakai di tingkat produksi (production-ready), kita menggunakan Layered / Clean Architecture dengan pembagian tanggung jawab yang jelas:
| Layer | Tanggung Jawab Utama |
|---|---|
Domain / Entity | Menampung struct model data utama database dan DTO. |
Repository | Melakukan operasi CRUD langsung ke Database SQL. |
Service | Menampung logika bisnis, validasi input, dan manajemen transaksi. |
Controller / Handler | Menerima HTTP Request, decode JSON, panggil Service, & encode HTTP Response. |
Router & Middleware | Routing endpoint URL dan penanganan error/auth terpusat. |
Contoh program
package main
import (
"fmt"
)
func main() {
fmt.Println("=== RESTFUL API CLEAN ARCHITECTURE ===")
fmt.Println("HTTP Request -> Router -> Controller -> Service -> Repository -> Database")
}
234. Model Entity & Data Transfer Object (Request & Response DTO)
Memisahkan struct internal database dengan struct payload JSON HTTP Request/Response.
Separation of DTO & Entity
Sangat disarankan untuk TIDAK memakai struct entity database secara langsung sebagai payload JSON HTTP. Gunakan DTO (Data Transfer Object) terpisah!
- Mengamankan data sensitif (seperti `password_hash` atau `internal_note`) agar tidak bocor di JSON Response.
- Memberikan tag validasi khusus pada Request DTO (seperti `validate:"required,email"`).
Contoh program
package main
import (
"fmt"
)
// Entity DB
type Category struct {
Id int
Name string
}
// Response DTO
type CategoryResponse struct {
Id int `json:"id"`
Name string `json:"name"`
}
func main() {
entity := Category{Id: 1, Name: "Gadget"}
dto := CategoryResponse{Id: entity.Id, Name: entity.Name}
fmt.Printf("Entity: %+v\n", entity)
fmt.Printf("Response DTO: %+v\n", dto)
}
235. Repository Layer & Database CRUD Operations
Implementasi interface CategoryRepository untuk kueri SQL `Save`, `Update`, `Delete`, `FindById`, `FindAll`.
Membangun Repository Layer
Repository menerima parameter context.Context dan koneksi database *sql.DB atau *sql.Tx untuk mengeksekusi prepared query SQL.
type CategoryRepository interface {
Save(ctx context.Context, tx *sql.Tx, category Category) Category
Update(ctx context.Context, tx *sql.Tx, category Category) Category
Delete(ctx context.Context, tx *sql.Tx, category Category)
FindById(ctx context.Context, tx *sql.Tx, categoryId int) (Category, error)
FindAll(ctx context.Context, tx *sql.Tx) []Category
}
Contoh program
package main
import (
"fmt"
)
type Category struct {
Id int
Name string
}
type MemoryCategoryRepo struct {
data map[int]Category
}
func (r *MemoryCategoryRepo) Save(c Category) Category {
r.data[c.Id] = c
return c
}
func main() {
repo := &MemoryCategoryRepo{data: make(map[int]Category)}
saved := repo.Save(Category{Id: 101, Name: "Laptop"})
fmt.Println("Saved to Repository:", saved)
}
236. Helper Transaksi Database (sql.Tx) Automatis
Membuat fungsi helper untuk manajemen Commit dan Rollback transaksi database secara aman.
Helper Transaksi Database
Untuk memastikan prinsip ACID pada database, operasi mutasi data harus dibungkus dalam sql.Tx. Kita buat helper function CommitOrRollback(tx *sql.Tx) menggunakan defer dan recover().
func CommitOrRollback(tx *sql.Tx) {
err := recover()
if err != nil {
tx.Rollback()
panic(err)
} else {
tx.Commit()
}
}
Contoh program
package main
import (
"fmt"
)
func DemoTransaction(shouldFail bool) {
fmt.Println("BEGIN TRANSACTION")
defer func() {
if r := recover(); r != nil {
fmt.Println("⚠️ ROLLBACK TRANSACTION! Error detected:", r)
} else {
fmt.Println("✅ COMMIT TRANSACTION SUCCESS!")
}
}()
if shouldFail {
panic("Database constraints violated!")
}
fmt.Println("EXECUTING SQL QUERIES...")
}
func main() {
DemoTransaction(false)
fmt.Println()
DemoTransaction(true)
}
237. Service Layer & Input Validation (go-playground/validator)
Implementasi logika bisnis dan validasi struct input request dengan library validator.
Service Layer & Validasi Input
Service Layer bertindak sebagai tempat logika bisnis dijalankan. Sebelum mengirim data ke Repository, Service melakukan validasi input menggunakan library populer go-playground/validator/v10.
type CreateCategoryRequest struct {
Name string `validate:"required,min=2,max=100" json:"name"`
}
Contoh program
package main
import (
"fmt"
"strings"
)
type CreateCategoryRequest struct {
Name string
}
func ValidateRequest(req CreateCategoryRequest) error {
if strings.TrimSpace(req.Name) == "" {
return fmt.Errorf("field 'name' tidak boleh kosong")
}
return nil
}
func main() {
req1 := CreateCategoryRequest{Name: ""}
req2 := CreateCategoryRequest{Name: "Peralatan Rumah"}
fmt.Println("Req 1 Validation:", ValidateRequest(req1))
fmt.Println("Req 2 Validation:", ValidateRequest(req2))
}
238. Controller / Handler Layer & JSON Serialization
Menerima `http.Request`, decoding payload JSON, memanggil Service, dan merespons `http.ResponseWriter`.
Controller Layer
Controller bertugas mengurus protocol HTTP. Controller membaca body request menggunakan json.NewDecoder(r.Body).Decode(&req) dan mengembalikan response JSON seragam menggunakan WebResponse struct.
type WebResponse struct {
Code int `json:"code"`
Status string `json:"status"`
Data interface{} `json:"data"`
}
Contoh program
package main
import (
"encoding/json"
"fmt"
)
type WebResponse struct {
Code int `json:"code"`
Status string `json:"status"`
Data interface{} `json:"data"`
}
func main() {
res := WebResponse{
Code: 200,
Status: "OK",
Data: map[string]string{"message": "Kategori berhasil dibuat"},
}
jsonBytes, _ := json.MarshalIndent(res, "", " ")
fmt.Println(string(jsonBytes))
}
239. Centralized Error Handling & Panic Recovery Middleware
Menangani error secara terpusat menggunakan Custom Error Middleware untuk merespon HTTP Status Code yang sesuai.
Centralized Error Handling
Daripada menuliskan if err != nil dengan response error JSON yang berulang di setiap controller, kita dapat melempar panic(err) di controller dan ditangkap oleh **ErrorHandler Middleware** terpusat.
ValidationError-> 400 Bad RequestNotFoundError-> 404 Not FoundInternalError / Unknown Panic-> 500 Internal Server Error
Contoh program
package main
import (
"fmt"
)
type NotFoundError struct {
Message string
}
func (e *NotFoundError) Error() string { return e.Message }
func ErrorMiddleware(fn func()) {
defer func() {
if r := recover(); r != nil {
if notFound, ok := r.(*NotFoundError); ok {
fmt.Println("HTTP 404 Not Found:", notFound.Error())
} else {
fmt.Println("HTTP 500 Internal Server Error:", r)
}
}
}()
fn()
}
func main() {
ErrorMiddleware(func() {
panic(&NotFoundError{Message: "Category ID #999 tidak ditemukan"})
})
}
240. HTTP Routing & Param Handling (julienschmidt/httprouter)
Mendaftarkan endpoint URL RESTful API menggunakan library HTTP Router performa tinggi.
High Performance Routing dengan HttpRouter
julienschmidt/httprouter adalah library HTTP router berbasis Radix Tree yang sangat cepat dan hemat memori di Golang.
router := httprouter.New()
router.GET("/api/categories", controller.FindAll)
router.GET("/api/categories/:categoryId", controller.FindById)
router.POST("/api/categories", controller.Create)
router.PUT("/api/categories/:categoryId", controller.Update)
router.DELETE("/api/categories/:categoryId", controller.Delete)
Contoh program
package main
import (
"fmt"
)
func main() {
fmt.Println("=== RESTFUL ENDPOINTS REGISTRATION ===")
fmt.Println("GET /api/categories -> FindAll")
fmt.Println("GET /api/categories/:id -> FindById")
fmt.Println("POST /api/categories -> Create")
fmt.Println("PUT /api/categories/:id -> Update")
fmt.Println("DELETE /api/categories/:id -> Delete")
}
241. Dependency Injection Assembly di Main Application
Merangkai DB Connection, Repository, Service, Controller, Middleware & HTTP Server pada `main.go`.
Merangkai Seluruh Komponen di main()
Tahap akhir sebelum menyalakan HTTP Server adalah memasangkan seluruh objek layer (Dependency Assembly):
func main() {
db := app.NewDB()
validate := validator.New()
categoryRepository := repository.NewCategoryRepository()
categoryService := service.NewCategoryService(categoryRepository, db, validate)
categoryController := controller.NewCategoryController(categoryService)
router := app.NewRouter(categoryController)
server := http.Server{
Addr: "localhost:3000",
Handler: middleware.NewAuthMiddleware(router),
}
server.ListenAndServe()
}
Contoh program
package main
import (
"fmt"
)
func main() {
fmt.Println("=== APPLICATION STARTUP ASSEMBLY ===")
fmt.Println("1. Database Connection initialized")
fmt.Println("2. Repositories & Services wired")
fmt.Println("3. Controllers & Router attached")
fmt.Println("4. HTTP Server listening on http://localhost:3000")
}
242. Integration Test RESTful API (net/http/httptest)
Pengujian otomatis endpoint REST API menggunakan package bawaan `net/http/httptest` dan assert response JSON.
Integration Test dengan `httptest`
Go menyediakan package bawaan net/http/httptest yang memungkinkan kita mengirim simulasi HTTP Request langsung ke handler HTTP tanpa perlu benar-benar menyalakan koneksi network port server!
func TestCreateCategorySuccess(t *testing.T) {
db := setupTestDB()
router := setupRouter(db)
requestBody := strings.NewReader(`{"name": "Gadget"}`)
request := httptest.NewRequest(http.MethodPost, "http://localhost:3000/api/categories", requestBody)
request.Header.Add("Content-Type", "application/json")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
response := recorder.Result()
assert.Equal(t, 200, response.StatusCode)
}
Contoh program
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
// Memodelkan test request menggunakan httptest
req := httptest.NewRequest(http.MethodGet, "/api/categories", nil)
rec := httptest.NewRecorder()
fmt.Println("=== INTEGRATION TEST SIMULATION ===")
fmt.Println("Simulated Method :", req.Method)
fmt.Println("Simulated Target :", req.URL.Path)
fmt.Println("Test Recorder Code:", rec.Code)
}