Membuat REST API di Golang dengan Clean Architecture

Clean Architecture adalah sebuah pendekatan untuk memisahkan kode berdasarkan tanggung jawabnya (separation of concerns), sehingga kode lebih mudah dibaca, dites, dan di-maintain. Pada artikel ini, kita akan membangun REST API sederhana menggunakan Golang dengan pola Clean Architecture.


Apa itu Clean Architecture?

Clean Architecture diperkenalkan oleh Uncle Bob (Robert C. Martin). Intinya, aplikasi dibagi ke dalam lapisan-lapisan berikut:

  1. Entities → Berisi business object atau model inti.
  2. Use Case → Logika aplikasi (aturan bisnis).
  3. Interface/Delivery → Bagian presentasi atau cara aplikasi diakses (contoh: REST API).
  4. Infrastructure/Repository → Implementasi akses data (database, API eksternal, dsb).

Struktur folder biasanya seperti ini:

project/
│── cmd/                # entrypoint (main.go)
│── internal/
│    ├── entity/        # definisi struct, model
│    ├── usecase/       # logika bisnis
│    ├── repository/    # interface & implementasi database
│    └── delivery/      # handler, controller (REST API)
│── pkg/                # helper, util

Membuat REST API Todo List

Kita akan membuat API sederhana untuk Todo List dengan fitur:

  • Create Todo
  • Get All Todos
  • Get Todo by ID

1. Entity (Model)

internal/entity/todo.go

package entity

type Todo struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

2. Repository (Abstraksi Akses Data)

internal/repository/todo_repository.go

package repository

import "myapp/internal/entity"

type TodoRepository interface {
    GetAll() ([]entity.Todo, error)
    GetByID(id int) (*entity.Todo, error)
    Create(todo entity.Todo) error
}

internal/repository/todo_memory.go (implementasi sementara dengan memory)

package repository

import "myapp/internal/entity"

type TodoMemory struct {
    todos []entity.Todo
}

func NewTodoMemory() *TodoMemory {
    return &TodoMemory{
        todos: []entity.Todo{},
    }
}

func (r *TodoMemory) GetAll() ([]entity.Todo, error) {
    return r.todos, nil
}

func (r *TodoMemory) GetByID(id int) (*entity.Todo, error) {
    for _, t := range r.todos {
        if t.ID == id {
            return &t, nil
        }
    }
    return nil, nil
}

func (r *TodoMemory) Create(todo entity.Todo) error {
    r.todos = append(r.todos, todo)
    return nil
}

3. Use Case (Logika Bisnis)

internal/usecase/todo_usecase.go

package usecase

import (
    "myapp/internal/entity"
    "myapp/internal/repository"
)

type TodoUsecase struct {
    repo repository.TodoRepository
}

func NewTodoUsecase(r repository.TodoRepository) *TodoUsecase {
    return &TodoUsecase{repo: r}
}

func (u *TodoUsecase) GetAll() ([]entity.Todo, error) {
    return u.repo.GetAll()
}

func (u *TodoUsecase) GetByID(id int) (*entity.Todo, error) {
    return u.repo.GetByID(id)
}

func (u *TodoUsecase) Create(todo entity.Todo) error {
    return u.repo.Create(todo)
}

4. Delivery (Handler API)

internal/delivery/todo_handler.go

package delivery

import (
    "encoding/json"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"
    "myapp/internal/entity"
    "myapp/internal/usecase"
)

type TodoHandler struct {
    usecase *usecase.TodoUsecase
}

func NewTodoHandler(u *usecase.TodoUsecase) *TodoHandler {
    return &TodoHandler{usecase: u}
}

func (h *TodoHandler) RegisterRoutes(r *mux.Router) {
    r.HandleFunc("/todos", h.GetAll).Methods("GET")
    r.HandleFunc("/todos/{id}", h.GetByID).Methods("GET")
    r.HandleFunc("/todos", h.Create).Methods("POST")
}

func (h *TodoHandler) GetAll(w http.ResponseWriter, r *http.Request) {
    todos, _ := h.usecase.GetAll()
    json.NewEncoder(w).Encode(todos)
}

func (h *TodoHandler) GetByID(w http.ResponseWriter, r *http.Request) {
    idStr := mux.Vars(r)["id"]
    id, _ := strconv.Atoi(idStr)
    todo, _ := h.usecase.GetByID(id)
    if todo == nil {
        w.WriteHeader(http.StatusNotFound)
        return
    }
    json.NewEncoder(w).Encode(todo)
}

func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
    var todo entity.Todo
    json.NewDecoder(r.Body).Decode(&todo)
    h.usecase.Create(todo)
    w.WriteHeader(http.StatusCreated)
}

5. Entrypoint

cmd/main.go

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
    "myapp/internal/delivery"
    "myapp/internal/repository"
    "myapp/internal/usecase"
)

func main() {
    repo := repository.NewTodoMemory()
    uc := usecase.NewTodoUsecase(repo)
    handler := delivery.NewTodoHandler(uc)

    r := mux.NewRouter()
    handler.RegisterRoutes(r)

    log.Println("Server running on :8080")
    http.ListenAndServe(":8080", r)
}

Uji Coba API

Jalankan perintah:

go run cmd/main.go

Lalu coba endpoint dengan curl atau Postman:

# Tambah todo
curl -X POST http://localhost:8080/todos -d '{"id":1,"title":"Belajar Golang","done":false}' -H "Content-Type: application/json"

# Ambil semua todo
curl http://localhost:8080/todos

# Ambil todo by ID
curl http://localhost:8080/todos/1