Arsitektur REST API yang Scalable dengan Go: Dari Desain hingga Production

I Nyoman Donostia 782 kata
Arsitektur REST API yang Scalable dengan Go: Dari Desain hingga Production

Kenapa Go untuk REST API?

Go (Golang) menjadi pilihan utama untuk membangun REST API skala enterprise karena tiga alasan fundamental: concurrency native via goroutines, compiled binary tanpa dependency eksternal, dan execution speed yang luar biasa. Dari pengalaman saya membangun API dengan Go di perusahaan, startup time container turun dari 5 detik (Node.js) ke 200ms (Go binary).

Project Structure yang Ideal

Untuk API skala enterprise, struktur folder harus memisahkan concern dengan jelas:

myapi/
├── cmd/                    # Entry point
│   └── server/
│       └── main.go
├── internal/               # Private application code
│   ├── handler/            # HTTP handlers
│   ├── service/            # Business logic
│   ├── repository/         # Data access
│   ├── model/               # Domain models
│   └── config/              # Configuration
├── pkg/                    # Public/reusable packages
│   ├── response/           # Response helpers
│   └── validator/          # Input validation
├── api/
│   └── openapi.yaml        # API documentation
├── migrations/             # Database migrations
├── Dockerfile
└── go.mod

Aturan kunci: kode di internal/ tidak bisa di-import dari luar module Go. Ini melindungi layer bisnis Anda dari paket eksternal.

Dependency Injection: Manual vs Wire

Manual DI: Sederhana & Explicit

// cmd/server/main.go
func main() {
    cfg := config.Load()
    db := database.Connect(cfg.DatabaseURL)
    
    userRepo := repository.NewUserRepository(db)
    userService := service.NewUserService(userRepo)
    userHandler := handler.NewUserHandler(userService)
    
    router := mux.NewRouter()
    router.HandleFunc("/users", userHandler.List).Methods("GET")
    
    log.Fatal(http.ListenAndServe(":8080", router))
}

Wire (Google): Untuk Project Besar

// wire.go
//go:build wireinject

func InitializeApp(cfg *config.Config) (*App, error) {
    wire.Build(
        database.Connect,
        repository.NewUserRepository,
        service.NewUserService,
        handler.NewUserHandler,
        NewApp,
    )
    return nil, nil
}

Wire menggenerate kode DI otomatis saat build. Pilihan yang disarankan kalau aplikasi Anda punya > 20 service.

Middleware: Auth, Rate Limit, dan CORS

1. JWT Authentication Middleware

// internal/handler/middleware/auth.go
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tokenString := r.Header.Get("Authorization")
        if tokenString == "" {
            response.Error(w, http.StatusUnauthorized, "Token required")
            return
        }
        
        token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
            return []byte(os.Getenv("JWT_SECRET")), nil
        })
        if err != nil || !token.Valid {
            response.Error(w, http.StatusUnauthorized, "Invalid token")
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

2. Rate Limiting dengan Token Bucket

// internal/handler/middleware/ratelimit.go
func RateLimitMiddleware(limiter *rate.Limiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                response.Error(w, http.StatusTooManyRequests, "Rate limit exceeded")
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// Init
limiter := rate.NewLimiter(rate.Every(time.Second), 10) // 10 req/sec

3. CORS Policy

import "github.com/rs/cors"

c := cors.New(cors.Options{
    AllowedOrigins:   []string{"https://app.example.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
    AllowedHeaders:   []string{"Authorization", "Content-Type"},
    AllowCredentials: true,
})
handler := c.Handler(router)

Database Layer: Repository Pattern

Pisahkan akses database dari business logic dengan repository pattern:

// internal/repository/user.go
type UserRepository interface {
    Create(ctx context.Context, user *model.User) error
    FindByID(ctx context.Context, id int64) (*model.User, error)
    List(ctx context.Context, limit, offset int) ([]*model.User, error)
}

type userRepository struct {
    db *sqlx.DB
}

func NewUserRepository(db *sqlx.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(ctx context.Context, id int64) (*model.User, error) {
    var user model.User
    err := r.db.GetContext(ctx, &user, 
        "SELECT id, email, name, created_at FROM users WHERE id = $1", id)
    if err != nil {
        return nil, fmt.Errorf("user not found: %w", err)
    }
    return &user, nil
}

Custom Error dan Response Format

Konsistensi response adalah pondasi API yang baik:

// pkg/response/response.go
type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Details any    `json:"details,omitempty"`
}

type APIResponse struct {
    Success bool `json:"success"`
    Data    any  `json:"data,omitempty"`
    Error   *APIError `json:"error,omitempty"`
    Meta    *Meta `json:"meta,omitempty"`
}

func Success(w http.ResponseWriter, data any, meta *Meta) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(APIResponse{
        Success: true,
        Data: data,
        Meta: meta,
    })
}

func Error(w http.ResponseWriter, code int, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(APIResponse{
        Error: &APIError{Code: code, Message: msg},
    })
}

Graceful Shutdown

func run() error {
    srv := &http.Server{Addr: ":8080", Handler: router}
    
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("server error: %v", err)
        }
    }()
    
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    return srv.Shutdown(ctx)
}

Dockerfile Multi-stage

# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server cmd/server/main.go

# Runtime stage
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/server /app/server
EXPOSE 8080
CMD ["/app/server"]

Final image: ~20MB, tanpa runtime interpreter.

Kesimpulan

Arsitektur REST API dengan Go yang baik memerlukan:

  1. Struktur folder yang memisahkan concern
  2. Dependency injection yang tidak over-engineered
  3. Middleware lengkap: auth, rate limit, logging
  4. Repository pattern untuk abstraction database
  5. Consistent response format
  6. Graceful shutdown untuk zero-downtime deploy
  7. Dockerfile multi-stage untuk image minimal

Hasil akhir: API yang cepat, secure, maintainable, dan siap scale ke traffic enterprise.

I Nyoman Donostia

I Nyoman Donostia

Fullstack Developer - Bali, Indonesia

Artikel Terkait