M
Mr Sugiarto
Developer
23 Aug 2026 5 min read

Model, migration, dan seeder sudah siap. Sekarang saatnya mengganti data statis di PostHandler dengan data sungguhan dari database, sehingga endpoint yang kita buat di Part 2 benar-benar berfungsi sebagai CRUD API.

Menyuntikkan Koneksi Database ke Handler

Sebelumnya PostHandler tidak punya dependency apapun. Sekarang kita tambahkan *gorm.DB supaya handler bisa query ke database:

type PostHandler struct {
	DB *gorm.DB
}

func NewPostHandler(db *gorm.DB) *PostHandler {
	return &PostHandler{DB: db}
}

Index — Menampilkan Semua Post

func (h *PostHandler) Index(c *fiber.Ctx) error {
	var posts []models.Post

	if err := h.DB.Order("id desc").Find(&posts).Error; err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "gagal mengambil data post",
		})
	}

	return c.JSON(fiber.Map{
		"data": posts,
	})
}

Find(&posts) mengambil semua baris dari tabel posts dan menyimpannya ke slice posts. Kita urutkan berdasarkan id terbaru dengan Order("id desc").

Show — Menampilkan Satu Post

func (h *PostHandler) Show(c *fiber.Ctx) error {
	var post models.Post

	id := c.Params("id")
	if err := h.DB.First(&post, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
				"message": "post tidak ditemukan",
			})
		}
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "gagal mengambil data post",
		})
	}

	return c.JSON(fiber.Map{
		"data": post,
	})
}

First(&post, id) mencari satu baris berdasarkan primary key. Kalau tidak ketemu, GORM mengembalikan error gorm.ErrRecordNotFound, yang kita tangkap khusus untuk membalas status 404.

Create — Membuat Post Baru

type postRequest struct {
	Title string `json:"title"`
	Slug  string `json:"slug"`
	Body  string `json:"body"`
}

func (h *PostHandler) Create(c *fiber.Ctx) error {
	var req postRequest
	if err := c.BodyParser(&req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"message": "body request tidak valid",
		})
	}

	post := models.Post{
		Title: req.Title,
		Slug:  req.Slug,
		Body:  req.Body,
	}

	if err := h.DB.Create(&post).Error; err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "gagal membuat post",
		})
	}

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{
		"message": "post berhasil dibuat",
		"data":    post,
	})
}

c.BodyParser(&req) membaca body request (JSON, form, atau multipart, tergantung Content-Type) dan memetakannya ke struct postRequest. Kita pakai struct request terpisah dari model supaya field yang boleh diisi dari luar jelas batasnya — ini akan lebih penting lagi begitu kita tambah validasi di Part 7.

Update & Delete

func (h *PostHandler) Update(c *fiber.Ctx) error {
	var post models.Post

	id := c.Params("id")
	if err := h.DB.First(&post, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
				"message": "post tidak ditemukan",
			})
		}
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "gagal mengambil data post",
		})
	}

	var req postRequest
	if err := c.BodyParser(&req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"message": "body request tidak valid",
		})
	}

	post.Title = req.Title
	post.Slug = req.Slug
	post.Body = req.Body

	if err := h.DB.Save(&post).Error; err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "gagal mengupdate post",
		})
	}

	return c.JSON(fiber.Map{
		"message": "post berhasil diupdate",
		"data":    post,
	})
}

func (h *PostHandler) Delete(c *fiber.Ctx) error {
	var post models.Post

	id := c.Params("id")
	if err := h.DB.First(&post, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
				"message": "post tidak ditemukan",
			})
		}
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "gagal mengambil data post",
		})
	}

	if err := h.DB.Delete(&post).Error; err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "gagal menghapus post",
		})
	}

	return c.SendStatus(fiber.StatusNoContent)
}

Karena model Post memakai gorm.Model (yang punya field DeletedAt), h.DB.Delete(&post) otomatis melakukan soft delete — baris tidak benar-benar dihapus, hanya kolom deleted_at diisi timestamp. Query Find/First selanjutnya otomatis mengabaikan baris yang sudah soft-deleted.

Menghubungkan DB ke Router

PostHandler sekarang butuh *gorm.DB saat dibuat, jadi routes.Setup dan main.go perlu diteruskan koneksinya:

// routes/routes.go
func Setup(app *fiber.App, db *gorm.DB) {
	postHandler := handlers.NewPostHandler(db)
	// ...
}
// main.go
db := database.Connect()
database.Migrate(db)
// ...
routes.Setup(app, db)

Testing End-to-End

Jalankan server:

go run main.go

Coba seluruh siklus CRUD:

# List
curl http://127.0.0.1:3000/api/v1/posts

# Create
curl -X POST http://127.0.0.1:3000/api/v1/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"Judul Post","slug":"judul-post","body":"Isi post"}'

# Show (ganti :id sesuai hasil create)
curl http://127.0.0.1:3000/api/v1/posts/11

# Update
curl -X PUT http://127.0.0.1:3000/api/v1/posts/11 \
  -H "Content-Type: application/json" \
  -d '{"title":"Judul Updated","slug":"judul-post","body":"Isi updated"}'

# Delete
curl -X DELETE http://127.0.0.1:3000/api/v1/posts/11

Setelah DELETE, request GET ke id yang sama akan membalas 404 post tidak ditemukan — tandanya soft delete dan penanganan not-found sudah bekerja dengan benar.

Sampai di sini, kita sudah punya REST API CRUD Post yang lengkap dan benar-benar tersambung ke MySQL. Di part berikutnya kita akan mulai masuk ke sisi autentikasi: register, login, dan JWT token.

M
Mr Sugiarto

Developer

Bagian dari Series: GoFiber Dasar - Belajar Fundamental Lewat REST API Blog

Belajar konsep-konsep dasar GoFiber (routing, handler, GORM, MySQL, JWT auth) dengan membangun REST API blog sederhana dari nol - lengkap dengan auten...

Lihat Series Lengkap