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.
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