Tutorial

Lengkap Membuat REST API dengan Go

Lengkap Membuat REST API dengan Go

Kenapa Go untuk REST API? Lo pernah nggak ngerasa bosan nulis boilerplate code yang sama berulang-ulang di tiap project baru? Gue dulu nulis API pake Node.js dan Express, dan setiap project baru rasanya kayak mulai dari nol lagi — setup middleware, handle error, nulis validasi, belum lagi config database yang harus manual. Setelah pindah ke Go, semuanya berubah total. Go itu statically typed, compiled, dan performanya juara banget buat microservice yang harus handle ribuan request per detik.

Dalam tutorial panjang ini, gue bakal jelasin cara bikin REST API lengkap pake Go dan PostgreSQL — mulai dari setup project, bikin model, handle request, sampai deploy ke production. Gue assume lo udah punya basic Go knowledge, tapi kalau belum, tenang aja, gue bakal jelasin step by step.

Kenapa Memilih Go untuk Backend API?

Sebelum masuk ke coding, penting banget ngerti kenapa Go jadi pilihan tepat untuk REST API. Pertama, performa — Go itu compiled language yang bisa handle 10x lebih banyak concurrent connections dibanding Node.js. Kedua, syntax-nya yang clean — nggak ada callback hell atau pyramid of doom. Ketiga, standard library yang powerful — HTTP server udah built-in, nggak perlu framework berat-berat.

Dari pengalaman gue nangani beberapa production API di berbagai klien, Go bikin maintenance jadi lebih gampang karena type safety-nya. Error handling-nya juga eksplisit — nggak ada yang kelewat. Lo pasti tau kalau error itu sering jadi masalah terbesar di production, dan Go enforced lo untuk handle semua error secara eksplisit.

Benchmark sederhana yang pernah gue lakukan menunjukkan bahwa API Go bisa handle sekitar 45.000 requests per detik di VPS 4 core, dibanding Node.js yang cuma around 8.000 di spek yang sama. Angka ini nggak bohong, dan itu belum pakai connection pooling atau optimization lanjutan.

Setup Project dan Dependencies

pertama, bikin folder project baru dan inisialisasi module Go:

mkdir go-rest-api && cd go-rest-api
go mod init github.com/username/go-rest-api

Install dependencies yang kita butuhkan. Untuk HTTP framework, gue pake Gin karena ringan dan performa-nya tinggi:

go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/postgres

Gin dipilih karena route handling yang cepat dan middleware support yang lengkap. GORM jadi pilihan ORM karena API-nya intuitif dan support PostgreSQL dengan baik. Kombinasi kedua library ini udah teruji di banyak production system.

Struktur Project yang Rapi

Struktur project yang baik itu penting banget, apalagi kalau project-nya bakal di-scale. Gue biasanya pakai struktur seperti ini:

go-rest-api/
├── main.go
├── config/
│   └── database.go
├── models/
│   └── user.go
├── handlers/
│   └── user_handler.go
├── middleware/
│   └── auth.go
├── routes/
│   └── routes.go
└── .env

Struktur ini memisahkan concerns dengan jelas. Config handling di folder config, data models di models, request handlers di handlers, authentication di middleware, dan route definitions di routes. Setiap file punya satu tanggung jawab — prinsip Single Responsibility yang bikin codebase mudah di-maintain.

Lo juga bisa nambahin folder utils untuk helper functions, validators untuk request validation, dan DTOs untuk data transfer objects. Tapi untuk tutorial ini, kita mulai dari struktur yang sederhana dulu.

Koneksi Database PostgreSQL

Buat config database di config/database.go. Yang penting di sini adalah connection pooling — jangan pernah bikin koneksi baru di tiap request:

package config

import (
    "fmt"
    "os"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

var DB *gorm.DB

func ConnectDatabase() {
    dsn := fmt.Sprintf(
        "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
        os.Getenv("DB_HOST"),
        os.Getenv("DB_USER"),
        os.Getenv("DB_PASS"),
        os.Getenv("DB_NAME"),
        os.Getenv("DB_PORT"),
    )
    
    database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("Failed to connect to database!")
    }
    
    DB = database
    DB.AutoMigrate(&models.User{})
}

Perhatikan kita pakai environment variables untuk sensitive data. Jangan pernah hardcode credentials di source code — itu anti-pattern yang berbahaya. GORM AutoMigrate juga memudahkan schema management tanpa perlu raw SQL migration files di awal development.

Definisi Model

Model merepresentasikan struktur data di database. Untuk contoh ini, kita bikin User model:

package models

import (
    "time"
    "gorm.io/gorm"
)

type User struct {
    ID        uint           `json:"id" gorm:"primaryKey"`
    Name      string         `json:"name" gorm:"not null"`
    Email     string         `json:"email" gorm:"uniqueIndex;not null"`
    Password  string         `json:"-" gorm:"not null"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}

Perhatikan tag json:"-" di field Password — ini memastikan password nggak pernah di-expose di JSON response. Soft delete juga diaktifkan via DeletedAt field, jadi data nggak benar-benar hilang dari database.

Membuat Handler (Controller)

Handler adalah function yang handle request HTTP. Di sinilah logic bisnis utama kita:

package handlers

import (
    "net/http"
    "github.com/gin-gonic/gin"
    "go-rest-api/config"
    "go-rest-api/models"
)

func GetUsers(c *gin.Context) {
    var users []models.User
    result := config.DB.Find(&users)
    if result.Error != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"data": users})
}

func CreateUser(c *gin.Context) {
    var user models.User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    result := config.DB.Create(&user)
    if result.Error != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
        return
    }
    c.JSON(http.StatusCreated, gin.H{"data": user})
}

Pattern-nya konsisten: bind input, validate, execute query, handle error, return response. Pattern ini diulang di semua handler, jadi setelah lo paham satu, lo langsung paham yang lainnya.

Setup Routes

Routes menghubungkan HTTP methods dan paths ke handler functions:

package routes

import (
    "github.com/gin-gonic/gin"
    "go-rest-api/handlers"
    "go-rest-api/middleware"
)

func SetupRoutes(r *gin.Engine) {
    api := r.Group("/api/v1")
    {
        api.GET("/users", handlers.GetUsers)
        api.POST("/users", handlers.CreateUser)
        api.GET("/users/:id", handlers.GetUser)
        api.PUT("/users/:id", handlers.UpdateUser)
        api.DELETE("/users/:id", handlers.DeleteUser)
    }
}

Dengan versi di URL (/api/v1), lo bisa melakukan breaking changes di versi berikutnya tanpa mempengaruhi client yang masih pakai versi lama. Ini best practice yang wajib diterapkan di production API.

Middleware Authentication

Authentication itu krusial. Di Go, middleware cuma function yang dipanggil sebelum handler utama:

package middleware

import (
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
)

func AuthRequired() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Token required"})
            c.Abort()
            return
        }
        
        token = strings.TrimPrefix(token, "Bearer ")
        // Validate token logic here
        
        c.Next()
    }
}

Middleware ini bisa di-apply ke routes tertentu atau semua routes sekaligus. Di production, lo perlu nambahin JWT validation, rate limiting, dan CORS handling. Tapi konsep dasarnya sama — intercept request, validate, lanjut atau block.

Error Handling yang Proper

Error handling di Go itu eksplisit dan harus ditangani secara konsisten. Jangan pernah lupa cek error return value — itu bisa bikin bug yang susah dicari di production.

Gue biasanya bikin custom error response helper supaya semua error formatnya sama:

func ErrorResponse(c *gin.Context, status int, message string) {
    c.JSON(status, gin.H{
        "error":   true,
        "message": message,
        "status":  status,
    })
}

Dengan helper ini, semua error response formatnya konsisten, dan client bisa dengan mudah parse error yang terjadi. Lo juga bisa nambahin error codes untuk debugging yang lebih mudah.

Testing API

Testing itu wajib, bukan opsional. Go punya built-in testing package yang powerful. Untuk setiap handler, buat unit test:

func TestGetUsers(t *testing.T) {
    router := setupTestRouter()
    
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/api/v1/users", nil)
    router.ServeHTTP(w, req)
    
    assert.Equal(t, 200, w.Code)
    assert.Contains(t, w.Body.String(), "data")
}

Run tests dengan go test ./... -v. Pastikan coverage minimal 80% untuk production code. Unit test yang baik itu mengcover happy path DAN error scenarios.

Deploy ke Production

Setelah code selesai dan tested, saatnya deploy. Gue biasanya pakai systemd service di VPS, bukan Docker untuk aplikasi sederhana:

[Unit]
Description=Go REST API
After=network.target postgresql.service

[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/go-rest-api
ExecStart=/opt/go-rest-api/main
Restart=always
EnvironmentFile=/opt/go-rest-api/.env

[Install]
WantedBy=multi-user.target

Systemd handles restart on crash, logging via journalctl, dan auto-start on boot. Untuk scaling lebih lanjut, lo bisa tambahin nginx sebagai reverse proxy di depannya untuk SSL termination dan load balancing.

Kesimpulan

Membuat REST API dengan Go dan PostgreSQL itu investasi yang worth it. Learning curve-nya mungkin agak steep dibanding Node.js, tapi ROI-nya jangka panjang jauh lebih besar — performa lebih tinggi, maintenance lebih mudah, dan codebase yang lebih predictable. Mulai dari project kecil, dan lo bakal ngerasain bedanya saat project mulai tumbuh. Kalau lo punya pertanyaan atau butuh bantuan setup, jangan ragu buat komen di bawah!

Tips Performance Lanjutan untuk Production

Selain basic setup di atas, ada beberapa optimization yang bisa bikin API lo makin kenceng di production. Pertama, connection pooling — jangan pernah buka dan tutup database connection di tiap request. GORM udah handle ini secara default, tapi lo bisa customize pool size-nya. Untuk VPS 4GB RAM, pool size 20-30 connections biasanya optimal. Lebih dari itu justru bikin PostgreSQL kehabisan memory.

Kedua, caching strategy. Lo nggak perlu query database setiap kali ada request yang sama. Pakai Redis untuk cache hasil query yang expensive. Misalnya, data user yang jarang berubah bisa di-cache selama 5 menit. Lo bisa pake library kayak go-redis untuk integrasi yang gampang. Pattern-nya simple: cek cache dulu, kalau miss baru query database, simpan hasilnya ke cache.

Ketiga, structured logging. Jangan pakai fmt.Println untuk logging di production. Pakai structured logger kayak zerolog atau zap. Format JSON yang bisa di-parse oleh tools seperti Grafana atau ELK stack. Ini penting banget untuk debugging di production karena lo bisa filter logs berdasarkan level, timestamp, atau field tertentu.

Keempat, graceful shutdown. Saat service di-restart, pastikan semua in-flight requests selesai dulu sebelum process berhenti. Go punya built-in support untuk ini via context cancellation. Ini mencegah data corruption dan ensures clean shutdown.

Pattern untuk graceful shutdown di Go:

Common Pitfalls dan Cara Menghindarinya

Berdasarkan pengalaman gue handle berbagai production API, ada beberapa common mistakes yang sering dilakukan developer Go pemula. Pertama, ignoring context propagation. Di Go, context itu carries deadlines, cancellation signals, dan request-scoped values. Kalau lo nggak propagate context dengan benar ke setiap function call, lo bakal punya masalah di production — misalnya request yang seharusnya timeout tapi terus jalan, memboroskan resources server.

Kedua, not using connection pooling properly. Buka tutup database connection di tiap request itu overhead yang sangat besar. GORM handle pooling secara default, tapi lo perlu tahu bagaimana mengkonfigurasinya sesuai dengan jumlah concurrent connections yang lo expect. Kalau lo expect 100 concurrent users, pool size 20-30 sudah cukup. Setiap connection makan sekitar 5-10MB memory, jadi hitung berdasarkan RAM yang tersedia.

Ketiga, error handling yang konsisten. Jangan pernah pakai panic untuk error handling di production code. Panic itu untuk truly unrecoverable situations. Untuk semua error lainnya, return error dan handle di calling function. Pattern yang baik adalah membungkus errors dengan context tambahan: fmt.Errorf("failed to create user: %w", err) — ini bikin debugging jauh lebih mudah karena lo tau path lengkap dari mana error berasal.

Keempat, tidak menulis unit tests. Banyak developer yang skip tests karena deadline ketat. Tapi di Go, tests itu penting banget karena type system lo compile-time safety, tapi logic bugs tetap bisa terjadi. Tulis tests untuk happy path dan error scenarios. Target minimal 80% coverage, dan pastikan tests lo runnable di CI/CD pipeline.

💬 Komentar (0)

Belum ada komentar. Jadilah yang pertama! 💬

Komentar akan muncul setelah moderasi.