📚 Series: Belajar Golang dari Nol sampai Deploy
← Part 5 Part 6: REST API dengan Gin Part 7: Database →

Kenapa Gin?

Gin adalah HTTP framework Go yang paling populer. Keunggulannya:

Setup Project

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

# Install Gin
go get github.com/gin-gonic/gin

Struktur Project

todo-api/
├── main.go
├── handlers/
│   └── task.go
├── models/
│   └── task.go
├── go.mod
└── go.sum

Model Task

Buat file models/task.go:

package models

import "time"

type Task struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Completed bool      `json:"completed"`
    CreatedAt time.Time `json:"created_at"`
}

type CreateTaskInput struct {
    Title string `json:"title" binding:"required,min=1,max=200"`
}

type UpdateTaskInput struct {
    Title     *string `json:"title" binding:"omitempty,min=1,max=200"`
    Completed *bool   `json:"completed"`
}

Handler

Buat file handlers/task.go:

package handlers

import (
    "net/http"
    "strconv"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/username/todo-api/models"
)

// Simulasi database dengan slice (sementara, sebelum pakai GORM di Part 7)
var tasks = []models.Task{}
var nextID = 1

func GetTasks(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "data":  tasks,
        "total": len(tasks),
    })
}

func GetTask(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ID tidak valid"})
        return
    }

    for _, task := range tasks {
        if task.ID == id {
            c.JSON(http.StatusOK, gin.H{"data": task})
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{"error": "Task tidak ditemukan"})
}

func CreateTask(c *gin.Context) {
    var input models.CreateTaskInput
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    task := models.Task{
        ID:        nextID,
        Title:     input.Title,
        Completed: false,
        CreatedAt: time.Now(),
    }
    nextID++
    tasks = append(tasks, task)

    c.JSON(http.StatusCreated, gin.H{
        "message": "Task berhasil dibuat",
        "data":    task,
    })
}

func UpdateTask(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ID tidak valid"})
        return
    }

    var input models.UpdateTaskInput
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    for i, task := range tasks {
        if task.ID == id {
            if input.Title != nil {
                tasks[i].Title = *input.Title
            }
            if input.Completed != nil {
                tasks[i].Completed = *input.Completed
            }
            c.JSON(http.StatusOK, gin.H{"data": tasks[i]})
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{"error": "Task tidak ditemukan"})
}

func DeleteTask(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ID tidak valid"})
        return
    }

    for i, task := range tasks {
        if task.ID == id {
            tasks = append(tasks[:i], tasks[i+1:]...)
            c.JSON(http.StatusOK, gin.H{"message": "Task berhasil dihapus"})
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{"error": "Task tidak ditemukan"})
}

Main — Setup Router

Edit main.go:

package main

import (
    "log"

    "github.com/gin-gonic/gin"
    "github.com/username/todo-api/handlers"
)

func main() {
    r := gin.Default()

    // Middleware CORS sederhana
    r.Use(func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization")
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        c.Next()
    })

    // Health check
    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    })

    // API v1
    v1 := r.Group("/api/v1")
    {
        tasks := v1.Group("/tasks")
        tasks.GET("", handlers.GetTasks)
        tasks.GET("/:id", handlers.GetTask)
        tasks.POST("", handlers.CreateTask)
        tasks.PATCH("/:id", handlers.UpdateTask)
        tasks.DELETE("/:id", handlers.DeleteTask)
    }

    log.Println("Server berjalan di http://localhost:8080")
    r.Run(":8080")
}

Jalankan dan Test

go run main.go

# Test dengan curl:
# Buat task baru
curl -X POST http://localhost:8080/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Belajar Go Part 6"}'

# Lihat semua task
curl http://localhost:8080/api/v1/tasks

# Update task (mark as done)
curl -X PATCH http://localhost:8080/api/v1/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

# Hapus task
curl -X DELETE http://localhost:8080/api/v1/tasks/1

Middleware Logging

// Custom middleware untuk log request
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next() // proses request
        duration := time.Since(start)
        log.Printf("[%d] %s %s — %v",
            c.Writer.Status(),
            c.Request.Method,
            c.Request.URL.Path,
            duration,
        )
    }
}

// Pakai di main.go
r.Use(Logger())

Checklist Progress

← Part 5 Part 7: Database →