M
Mr Sugiarto
Developer
02 Sep 2026 4 min read

Post yang punya gambar sampul jauh lebih menarik ditampilkan di halaman blog. Di part ini kita tambahkan endpoint untuk upload thumbnail, lengkap dengan validasi tipe file dan ukuran maksimal.

Menambahkan Kolom Thumbnail

Update models/post.go:

type Post struct {
	gorm.Model
	Title     string `gorm:"size:255;not null" json:"title"`
	Slug      string `gorm:"size:255;uniqueIndex;not null" json:"slug"`
	Body      string `gorm:"type:text" json:"body"`
	Thumbnail string `gorm:"size:255" json:"thumbnail,omitempty"`
	UserID    uint   `gorm:"index" json:"user_id"`
	Author    *User  `gorm:"foreignKey:UserID" json:"author,omitempty"`

	Categories []Category `gorm:"many2many:post_categories;" json:"categories,omitempty"`
}

Thumbnail cuma menyimpan path filenya, bukan file itu sendiri — file gambar aslinya disimpan di filesystem, bukan di database.

Menyiapkan Folder Upload & Static File Server

Buat folder public/uploads/ di root project (tambahkan file .gitkeep supaya foldernya ikut ter-track git walau kosong). Tambahkan ke .gitignore supaya file yang di-upload user tidak ikut ter-commit, tapi struktur foldernya tetap ada:

/public/uploads/*
!/public/uploads/.gitkeep

Fiber punya app.Static() untuk menyajikan file statis. Tambahkan di main.go:

app.Static("/uploads", "./public/uploads")

Dengan ini, file yang tersimpan di public/uploads/nama-file.png otomatis bisa diakses lewat http://localhost:3000/uploads/nama-file.png.

Endpoint Upload

Tambahkan ke handlers/post_handler.go:

const (
	thumbnailMaxSizeBytes = 2 << 20 // 2MB
	thumbnailUploadDir    = "./public/uploads"
)

var allowedThumbnailExt = map[string]bool{
	".jpg":  true,
	".jpeg": true,
	".png":  true,
}

func (h *PostHandler) UploadThumbnail(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 response.Error(c, fiber.StatusNotFound, "post tidak ditemukan")
		}
		return response.Error(c, fiber.StatusInternalServerError, "gagal mengambil data post")
	}

	if post.UserID != c.Locals("user_id").(uint) {
		return response.Error(c, fiber.StatusForbidden, "anda tidak berhak mengubah post ini")
	}

	file, err := c.FormFile("thumbnail")
	if err != nil {
		return response.Error(c, fiber.StatusBadRequest, "file thumbnail wajib diisi")
	}

	ext := strings.ToLower(filepath.Ext(file.Filename))
	if !allowedThumbnailExt[ext] {
		return response.Error(c, fiber.StatusUnprocessableEntity, "format file harus jpg, jpeg, atau png")
	}

	if file.Size > thumbnailMaxSizeBytes {
		return response.Error(c, fiber.StatusUnprocessableEntity, "ukuran file maksimal 2MB")
	}

	filename := fmt.Sprintf("%s%s", uuid.NewString(), ext)
	if err := c.SaveFile(file, filepath.Join(thumbnailUploadDir, filename)); err != nil {
		return response.Error(c, fiber.StatusInternalServerError, "gagal menyimpan file")
	}

	post.Thumbnail = "/uploads/" + filename
	if err := h.DB.Save(&post).Error; err != nil {
		return response.Error(c, fiber.StatusInternalServerError, "gagal memperbarui data post")
	}

	return response.Success(c, fiber.StatusOK, "thumbnail berhasil diupload", post)
}

Beberapa hal penting:

  • Sama seperti Update/Delete, endpoint ini memvalidasi kepemilikan post — hanya penulis yang boleh mengganti thumbnail-nya.
  • c.FormFile("thumbnail") membaca file dari multipart/form-data, bukan JSON body biasa — cara request-nya beda dari endpoint lain.
  • Ekstensi file divalidasi lewat whitelist (allowedThumbnailExt), bukan blacklist — lebih aman karena kita eksplisit menentukan apa yang boleh, bukan mencoba menebak apa yang harus diblokir.
  • Ukuran file dibatasi 2MB lewat pengecekan file.Size sebelum disimpan.
  • Nama file di-generate ulang pakai uuid.NewString() — bukan memakai nama asli dari user. Ini penting: nama file asli bisa berisi karakter aneh, bentrok dengan file lain, atau bahkan disusupi path traversal (../../etc/passwd) kalau dipakai mentah-mentah.

Menambahkan Route

posts.Post("/:id/thumbnail", middleware.AuthRequired, postHandler.UploadThumbnail)

Testing

# Upload thumbnail ke post milik sendiri
curl -X POST http://127.0.0.1:3000/api/v1/posts/30/thumbnail \
  -H "Authorization: Bearer $TOKEN" \
  -F "thumbnail=@/path/ke/gambar.png"

Response akan berisi path thumbnail yang baru:

{
  "message": "thumbnail berhasil diupload",
  "data": {
    "ID": 30,
    "thumbnail": "/uploads/9335d89d-9b8d-4141-8b9c-899aad5b13ff.png",
    ...
  }
}

Akses langsung filenya lewat browser atau curl:

curl -I http://127.0.0.1:3000/uploads/9335d89d-9b8d-4141-8b9c-899aad5b13ff.png

Coba juga upload file yang bukan gambar (misalnya .txt) — akan ditolak dengan 422 dan pesan "format file harus jpg, jpeg, atau png". Dan tanpa token sama sekali, request akan ditolak 401 seperti endpoint terproteksi lainnya.

Post sekarang bisa punya thumbnail. Di part berikutnya kita tambahkan sistem role — membedakan user biasa dengan admin, supaya beberapa aksi bisa dibatasi khusus admin.

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