Golang Murni Bikin REST API? Gampang Banget, Nih Contohnya


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
, danhandler
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()
diCommitOrRollback
? 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:
File
go.mod
telah mengimpor semua dependency sepertigithub.com/go-sql-driver/mysql
,github.com/julienschmidt/httprouter
,github.com/go-playground/validator/v10
, dll.Database
golang-restful-api
dan tabelhabits
sudah tersedia.
Lalu jalankan:
go run main.go
Jika berhasil, akan muncul:
๐ Server running on http://localhost:3000
Subscribe to my newsletter
Read articles from Ajitama Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
