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:
- Struktur folder yang memisahkan concern
- Dependency injection yang tidak over-engineered
- Middleware lengkap: auth, rate limit, logging
- Repository pattern untuk abstraction database
- Consistent response format
- Graceful shutdown untuk zero-downtime deploy
- Dockerfile multi-stage untuk image minimal
Hasil akhir: API yang cepat, secure, maintainable, dan siap scale ke traffic enterprise.