Golang Murni Bikin REST API? Gampang Banget, Nih Contohnya

Ajitama DevAjitama Dev
8 min read

Kadang orang mikir bikin REST API harus pake framework segede gaban. Padahal, pakai Golang murni juga bisa โ€” ringan, cepat, dan kita punya kontrol penuh. Di tutorial ini, kita bakal bikin REST API sederhana untuk habit tracker. Yuk langsung gas!


๐Ÿ› ๏ธ 1. Inisialisasi Proyek

Pertama, kita mulai dengan setup project Go-nya.

mkdir habit-tracker-api
cd habit-tracker-api
go mod init github.com/fardannozami/habit-tracker-api

๐Ÿ—ƒ๏ธ 2. Setup Database MySQL

Kita pake MySQL sebagai database. Jalankan perintah berikut di MySQL CLI:

CREATE DATABASE `habit-tracker-api`;
USE `habit-tracker-api`;

CREATE TABLE habits (
  id INT NOT NULL AUTO_INCREMENT,
  name VARCHAR(255) NOT NULL,
  description TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);

CREATE TABLE habit_checks (
  habit_id INT NOT NULL,
  check_date DATE NOT NULL,
  FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE,
  CONSTRAINT unique_habit_check UNIQUE (habit_id, check_date)
);

๐Ÿ“ฆ 3. Install Dependency MySQL Driver

go get github.com/go-sql-driver/mysql

๐Ÿ“ 4. Definisikan Model

Buat file model/habit.go:

package model

import "time"

type Habit struct {
    ID          int       `json:"id"`
    Name        string    `json:"name"`
    Description string    `json:"description"`
    CreatedAt   time.Time `json:"created_at"`
}

type HabitCheck struct {
    HabitID   int       `json:"habit_id"`
    CheckDate time.Time `json:"check_date"`
}

๐Ÿ“‚ 5. Buat Repository Pattern

Kenapa repository? Biar logic DB kita terpisah rapi dari business logic. Scalability dan testability lebih enak.

โœ… Habit Repository

Buat file repository/habit_repository.go:

package repository

import (
    "context"
    "database/sql"
    "time"

    "github.com/fardannozami/habit-tracker-api/helper"
    "github.com/fardannozami/habit-tracker-api/model"
)

type HabitRepository interface {
    GetAll(ctx context.Context, tx *sql.Tx) []model.Habit
    GetById(ctx context.Context, tx *sql.Tx, habitId int) model.Habit
    Create(ctx context.Context, tx *sql.Tx, habit model.Habit) model.Habit
    Update(ctx context.Context, tx *sql.Tx, habit model.Habit) model.Habit
    Delete(ctx context.Context, tx *sql.Tx, habit model.Habit)
}

type mysqlHabitRepository struct{}

func NewMysqlHabitRepository() HabitRepository {
    return &mysqlHabitRepository{}
}

func (r *mysqlHabitRepository) GetAll(ctx context.Context, tx *sql.Tx) []model.Habit {
    SQL := "SELECT id, name, description, created_at FROM habits"
    rows, err := tx.QueryContext(ctx, SQL)
    helper.PanicIfError(err)

    defer rows.Close()

    var habits []model.Habit
    for rows.Next() {
        habit := model.Habit{}
        err := rows.Scan(&habit.ID, &habit.Name, &habit.Description, &habit.CreatedAt)
        helper.PanicIfError(err)

        habits = append(habits, habit)
    }

    return habits
}

func (r *mysqlHabitRepository) GetById(ctx context.Context, tx *sql.Tx, habitId int) model.Habit {
    var habit model.Habit

    SQL := "SELECT id, name, description, created_at FROM habits WHERE id = ?"
    err := tx.QueryRowContext(ctx, SQL, habitId).Scan(&habit.ID, &habit.Name, &habit.Description, &habit.CreatedAt)
    helper.PanicIfError(err)

    return habit
}

func (r *mysqlHabitRepository) Create(ctx context.Context, tx *sql.Tx, habit model.Habit) model.Habit {
    habit.CreatedAt = time.Now()

    SQL := "INSERT INTO habits(name, description, created_at) VALUES(?, ?, ?)"
    result, err := tx.ExecContext(ctx, SQL, habit.Name, habit.Description, habit.CreatedAt)
    helper.PanicIfError(err)

    id, err := result.LastInsertId()
    helper.PanicIfError(err)

    habit.ID = int(id)

    return habit
}

func (r *mysqlHabitRepository) Update(ctx context.Context, tx *sql.Tx, habit model.Habit) model.Habit {
    SQL := "UPDATE habits SET name = ?, description = ? WHERE id = ?"
    _, err := tx.ExecContext(ctx, SQL, habit.Name, habit.Description, habit.ID)
    helper.PanicIfError(err)

    return habit
}

func (r *mysqlHabitRepository) Delete(ctx context.Context, tx *sql.Tx, habit model.Habit) {
    SQL := "DELETE FROM habits WHERE id = ?"
    _, err := tx.ExecContext(ctx, SQL, habit.ID)
    helper.PanicIfError(err)
}

๐Ÿ”ง Semua operasi menggunakan transaction (*sql.Tx) agar mudah dikontrol dari service layer, apalagi buat rollback kalau error.


๐Ÿง  Best Practice Note

  • โœ… Gunakan context agar setiap query aware terhadap cancellation atau timeout dari request.

  • โœ… Bungkus semua query dalam transaction biar atomic.

  • โœ… Pisahkan model, repository, service, dan handler agar kode lebih mudah diuji dan dimaintain.

  • โŒ Hindari SQL hardcoded di seluruh tempat โ€” simpan di layer repository saja.



๐Ÿ”ง 6. Bangun Service Layer: habit_service

Service layer ini jadi penghubung antara controller (handler HTTP) dengan repository. Di sini kita bisa validasi request, handle transaction, dan atur alur bisnis.

Sebelum buat service kita install dulu dependency untuk validasinya

go get github.com/go-playground/validator/v10

Interface HabitService

package service

import (
    "context"
    "database/sql"

    "github.com/go-playground/validator/v10"
    "github.com/fardannozami/habit-tracker-api/model"
    "github.com/fardannozami/habit-tracker-api/repository"
    "github.com/fardannozami/habit-tracker-api/request"
    "github.com/fardannozami/habit-tracker-api/response"
    "github.com/fardannozami/habit-tracker-api/helper"
)

type HabitService interface {
    GetAll(ctx context.Context) []response.HabitResponse
    GetById(ctx context.Context, habitId int) response.HabitResponse
    Create(ctx context.Context, request request.HabitCreateRequest) response.HabitResponse
    Update(ctx context.Context, request request.HabitUpdateRequest) response.HabitResponse
    Delete(ctx context.Context, habitId int)
}

Implementasi habitService

type habitService struct {
    habitRepository repository.HabitRepository
    dB              *sql.DB
    validate        *validator.Validate
}

func NewHabitService(habitRepository repository.HabitRepository, db *sql.DB, validate *validator.Validate) HabitService {
    return &habitService{
        habitRepository: habitRepository,
        dB:              db,
        validate:        validate,
    }
}

func (s *habitService) GetAll(ctx context.Context) []response.HabitResponse {
    tx, err := s.dB.Begin()
    helper.PanicIfError(err)

    defer helper.CommitOrRollback(tx)

    habits := s.habitRepository.GetAll(ctx, tx)

    return helper.ToHabitResponses(habits)
}

func (s *habitService) GetById(ctx context.Context, habitId int) response.HabitResponse {
    tx, err := s.dB.Begin()
    helper.PanicIfError(err)

    defer helper.CommitOrRollback(tx)

    habit := s.habitRepository.GetById(ctx, tx, habitId)

    return helper.ToHabitResponse(habit)

}

func (s *habitService) Create(ctx context.Context, request request.HabitCreateRequest) response.HabitResponse {
    var habitResponse response.HabitResponse
    err := s.validate.Struct(request)
    helper.PanicIfError(err)

    tx, err := s.dB.Begin()
    helper.PanicIfError(err)

    habit := model.Habit{Name: request.Name, Description: request.Description}
    defer helper.CommitOrRollback(tx)

    savedHabit := s.habitRepository.Create(ctx, tx, habit)

    habitResponse = helper.ToHabitResponse(savedHabit)

    return habitResponse
}

func (s *habitService) Update(ctx context.Context, request request.HabitUpdateRequest) response.HabitResponse {
    err := s.validate.Struct(request)
    helper.PanicIfError(err)

    tx, err := s.dB.Begin()
    helper.PanicIfError(err)

    defer helper.CommitOrRollback(tx)

    habit := s.habitRepository.GetById(ctx, tx, request.ID)

    habit.Name = request.Name
    habit.Description = request.Description

    habit = s.habitRepository.Update(ctx, tx, habit)

    return helper.ToHabitResponse(habit)
}

func (s *habitService) Delete(ctx context.Context, habitId int) {
    tx, err := s.dB.Begin()
    helper.PanicIfError(err)

    defer helper.CommitOrRollback(tx)

    habit := s.habitRepository.GetById(ctx, tx, habitId)

    s.habitRepository.Delete(ctx, tx, habit)
}

Semua method-nya pakai transaction (dari sql.DB) dan konversi response pakai helper. Struktur ini sangat fleksibel buat testing juga.


๐Ÿงฐ 7. Buat Helper Utility

File helper/helper.go:

package helper

import (
    "database/sql"

    "github.com/fardannozami/habit-tracker-api/model"
    "github.com/fardannozami/habit-tracker-api/response"
)

func PanicIfError(err error) {
    if err != nil {
        panic(err.Error())
    }
}

func CommitOrRollback(tx *sql.Tx) {
    err := recover()
    if err != nil {
        errRollback := tx.Rollback()
        PanicIfError(errRollback)
        panic(err)
    } else {
        errCommit := tx.Commit()
        PanicIfError(errCommit)
    }
}

func ToHabitResponse(habit model.Habit) response.HabitResponse {
    return response.HabitResponse{
        ID:          habit.ID,
        Name:        habit.Name,
        Description: habit.Description,
    }
}

func ToHabitResponses(habits []model.Habit) []response.HabitResponse {
    var responses []response.HabitResponse
    for _, habit := range habits {
        responses = append(responses, ToHabitResponse(habit))
    }
    return responses
}

๐Ÿ”ฅ Kenapa pakai recover() di CommitOrRollback? Ini supaya kita bisa handle panic dari dalam service (misal error validasi atau SQL), lalu rollback otomatis.


๐Ÿ“จ 8. Request & Response DTO

Simpan di folder request/ dan response/.

request/habit_request.go

package request

type HabitCreateRequest struct {
    Name        string `json:"name" validate:"required"`
    Description string `json:"description" validate:"required"`
}

type HabitUpdateRequest struct {
    ID          int    `json:"id" validate:"required"`
    Name        string `json:"name" validate:"required"`
    Description string `json:"description" validate:"required"`
}

response/habit_response.go

package response

type HabitResponse struct {
    ID          int    `json:"id"`
    Name        string `json:"name"`
    Description string `json:"description"`
}

๐Ÿง  Best Practice Insight

  • โœ… Validation di service layer: biar centralized dan bisa custom behavior-nya.

  • โœ… Pemisahan DTO dan model: model mewakili struktur database, sedangkan DTO (request/response) mewakili struktur API.

  • โœ… Transaction-per-request: bikin semua operasi atomic, terutama kalau nanti nambah fitur kompleks (multiple insert/update dalam 1 request).

  • ๐Ÿ”’ Error handling pakai panic + recover cukup oke untuk skala kecil, tapi nanti kita bisa refactor pakai custom error.


๐Ÿ“ฆ 9. Bangun HTTP Controller: habit_controller.go

Interface dan Struct

type HabitController interface {
    Create(http.ResponseWriter, *http.Request, httprouter.Params)
    Update(http.ResponseWriter, *http.Request, httprouter.Params)
    Delete(http.ResponseWriter, *http.Request, httprouter.Params)
    GetAll(http.ResponseWriter, *http.Request, httprouter.Params)
    GetById(http.ResponseWriter, *http.Request, httprouter.Params)
}

type habitController struct {
    habitService service.HabitService
}

func NewHabitController(habitService service.HabitService) HabitController {
    return &habitController{habitService: habitService}
}

โš™๏ธ Handler Implementasi

Create

func (c *habitController) Create(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
    var habitCreateRequest request.HabitCreateRequest
    err := json.NewDecoder(req.Body).Decode(&habitCreateRequest)
    helper.PanicIfError(err)

    habit := c.habitService.Create(req.Context(), habitCreateRequest)

    writeJsonResponse(w, http.StatusCreated, "created", habit)
}

Update

func (c *habitController) Update(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
    var habitUpdateRequest request.HabitUpdateRequest
    err := json.NewDecoder(req.Body).Decode(&habitUpdateRequest)
    helper.PanicIfError(err)

    habitId, err := strconv.Atoi(params.ByName("id"))
    helper.PanicIfError(err)
    habitUpdateRequest.ID = habitId

    habit := c.habitService.Update(req.Context(), habitUpdateRequest)

    writeJsonResponse(w, http.StatusOK, "success", habit)
}

Delete

func (c *habitController) Delete(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
    habitId, err := strconv.Atoi(params.ByName("id"))
    helper.PanicIfError(err)

    c.habitService.Delete(req.Context(), habitId)

    writeJsonResponse(w, http.StatusOK, "success", nil)
}

GetAll

func (c *habitController) GetAll(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
    habits := c.habitService.GetAll(req.Context())
    writeJsonResponse(w, http.StatusOK, "success", habits)
}

GetById

func (c *habitController) GetById(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
    habitId, err := strconv.Atoi(params.ByName("id"))
    helper.PanicIfError(err)

    habit := c.habitService.GetById(req.Context(), habitId)
    writeJsonResponse(w, http.StatusOK, "success", habit)
}

๐Ÿ“ค 10. API Response Wrapper

Simpan di response/api_response.go:

package response

type ApiResponse struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

Tambahkan helper untuk response writer di helper/json_response.go:

package helper

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

    "github.com/fardannozami/habit-tracker-api/response"
)

func writeJsonResponse(w http.ResponseWriter, code int, message string, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)

    apiResponse := response.ApiResponse{
        Code:    code,
        Message: message,
        Data:    data,
    }

    err := json.NewEncoder(w).Encode(apiResponse)
    PanicIfError(err)
}

๐ŸŽฏ Ini bikin respons kamu konsisten, DRY, dan siap untuk error handling juga nanti.


โœ… Summary

Controller ini:

  • Fokus pada parsing dan routing request.

  • Delegasi semua logic ke service layer.

  • Mengembalikan JSON response yang konsisten.

  • Pakai httprouter untuk routing yang performa tinggi.



๐Ÿ›ฃ๏ธ 11. Setup Routing

File: router/habit_router.go

package router

import (
    "github.com/fardannozami/habit-tracker-api/controller"
    "github.com/julienschmidt/httprouter"
)

func HabitRoutes(router *httprouter.Router, habitController controller.HabitController) {
    router.GET("/api/habits", habitController.GetAll)
    router.GET("/api/habits/:id", habitController.GetById)
    router.POST("/api/habits", habitController.Create)
    router.PUT("/api/habits/:id", habitController.Update)
    router.DELETE("/api/habits/:id", habitController.Delete)
}

๐Ÿง  12. main.go โ€“ Setup Seluruh Komponen & Jalankan Server

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "time"

    "github.com/go-playground/validator/v10"
    "github.com/julienschmidt/httprouter"

    "github.com/fardannozami/habit-tracker-api/app"
    "github.com/fardannozami/habit-tracker-api/controller"
    "github.com/fardannozami/habit-tracker-api/helper"
    "github.com/fardannozami/habit-tracker-api/repository"
    "github.com/fardannozami/habit-tracker-api/router"
    "github.com/fardannozami/habit-tracker-api/service"
)

func main() {
    validate := validator.New()

    db, err := app.NewMySqlDB()
    helper.PanicIfError(err)

    // Repository
    habitRepository := repository.NewMysqlHabitRepository()
    habitCheckRepository := repository.NewHabitCheckRepository()

    // Service
    habitService := service.NewHabitService(habitRepository, db, validate)
    habitCheckService := service.NewHabitCheckService(db, habitCheckRepository, habitRepository, validate)

    // Controller
    habitController := controller.NewHabitController(habitService)
    habitCheckController := controller.NewHabitCheckController(habitCheckService)

    // Router
    r := httprouter.New()
    router.HabitRoutes(r, habitController)
    router.HabitCheckRoutes(r, habitCheckController)

    port := os.Getenv("PORT")
    if port == "" {
        port = "3000"
    }

    server := &http.Server{
        Addr:    ":" + port,
        Handler: r,
    }

    // Jalankan server di goroutine
    go func() {
        fmt.Println("๐Ÿš€ Server running on http://localhost:" + port)
        helper.PanicIfError(server.ListenAndServe())
    }()

    // Graceful shutdown
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)
    <-quit

    fmt.Println("\n๐Ÿ›‘ Server shutting down...")
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        fmt.Printf("โŒ Server forced to shutdown: %s\n", err)
    } else {
        fmt.Println("โœ… Server exited gracefully")
    }
}

๐Ÿ”Œ 13. Koneksi ke Database

File: app/database.go

package app

import (
    "database/sql"
    "fmt"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

func NewMySqlDB() (*sql.DB, error) {
    dsn := "root:@tcp(127.0.0.1:3306)/golang-restful-api?parseTime=true"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return nil, fmt.Errorf("failed to open DB: %w", err)
    }

    if err := db.Ping(); err != nil {
        return nil, fmt.Errorf("failed to connect to DB: %w", err)
    }

    db.SetMaxIdleConns(5)
    db.SetMaxOpenConns(20)
    db.SetConnMaxLifetime(60 * time.Minute)
    db.SetConnMaxIdleTime(10 * time.Minute)

    return db, nil
}

๐Ÿงช 14. Tes Jalankan Server

Pastikan:

Lalu jalankan:

go run main.go

Jika berhasil, akan muncul:

๐Ÿš€ Server running on http://localhost:3000

0
Subscribe to my newsletter

Read articles from Ajitama Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ajitama Dev
Ajitama Dev