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.
Clean Architecture diperkenalkan oleh Uncle Bob (Robert C. Martin). Intinya, aplikasi dibagi ke dalam lapisan-lapisan berikut:
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
Kita akan membuat API sederhana untuk Todo List dengan fitur:
internal/entity/todo.go
package entity
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
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
}
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)
}
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)
}
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)
}
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