M
Mr Sugiarto
Developer
25 Aug 2026 4 min read

Setelah di part sebelumnya kita berhasil menghubungkan Post dengan penulisnya (User), sekarang kita akan menambahkan fitur yang cukup umum di aplikasi blog: Comment. Pembaca (user yang sudah login) bisa berkomentar di sebuah post, dan siapa saja boleh membaca daftar komentarnya.

Migration Tabel Comments

go run . artisan make:migration create_comments_table
package migrations

import (
	"github.com/goravel/framework/contracts/database/schema"
	"github.com/goravel/framework/facades"
)

type M20260819000001CreateCommentsTable struct{}

func (r *M20260819000001CreateCommentsTable) Signature() string {
	return "20260819000001_create_comments_table"
}

func (r *M20260819000001CreateCommentsTable) Up() error {
	if !facades.Schema().HasTable("comments") {
		return facades.Schema().Create("comments", func(table schema.Blueprint) {
			table.ID()
			table.UnsignedBigInteger("post_id")
			table.UnsignedBigInteger("user_id")
			table.Text("body")
			table.TimestampsTz()
		})
	}

	return nil
}

func (r *M20260819000001CreateCommentsTable) Down() error {
	return facades.Schema().DropIfExists("comments")
}

Jangan lupa daftarkan migration ini ke database/kernel.go, baru jalankan go run . artisan migrate.

Membuat Model Comment

package models

import (
	"github.com/goravel/framework/database/orm"
)

type Comment struct {
	orm.Model
	PostID uint   `json:"post_id"`
	UserID uint   `json:"user_id"`
	Body   string `json:"body"`
	User   User   `json:"user,omitempty"`
}

Sama seperti relasi Post-User di part sebelumnya, field UserID + User di sini otomatis dikenali sebagai relasi belongs to — komentar dibuat oleh satu user.

Membuat CommentController

Buat controller baru app/http/controllers/comment_controller.go dengan tiga method: Index, Store, dan Destroy.

func (r *CommentController) Index(ctx http.Context) http.Response {
	postID := ctx.Request().Route("id")

	var post models.Post
	if err := facades.Orm().Query().Where("id", postID).First(&post); err != nil || post.ID == 0 {
		return helpers.Error(ctx, http.StatusNotFound, "Post not found", nil)
	}

	var comments []models.Comment
	if err := facades.Orm().Query().With("User").Where("post_id", postID).OrderBy("id", "desc").Find(&comments); err != nil {
		return helpers.Error(ctx, http.StatusInternalServerError, "Failed to retrieve comments", err.Error())
	}

	return helpers.Success(ctx, http.StatusOK, "Comments retrieved successfully", comments)
}

func (r *CommentController) Store(ctx http.Context) http.Response {
	postID := ctx.Request().Route("id")

	var post models.Post
	if err := facades.Orm().Query().Where("id", postID).First(&post); err != nil || post.ID == 0 {
		return helpers.Error(ctx, http.StatusNotFound, "Post not found", nil)
	}

	validator, err := ctx.Request().Validate(map[string]string{
		"body": "required",
	})

	if err != nil {
		return helpers.Error(ctx, http.StatusInternalServerError, "Failed to validate request", err.Error())
	}

	if validator.Fails() {
		return helpers.Error(ctx, http.StatusUnprocessableEntity, "Validation failed", validator.Errors().All())
	}

	comment := models.Comment{
		PostID: post.ID,
		UserID: helpers.AuthUserID(ctx),
		Body:   ctx.Request().Input("body"),
	}

	if err := facades.Orm().Query().Create(&comment); err != nil {
		return helpers.Error(ctx, http.StatusBadRequest, "Failed to create comment", err.Error())
	}

	return helpers.Success(ctx, http.StatusCreated, "Comment created successfully", comment)
}

func (r *CommentController) Destroy(ctx http.Context) http.Response {
	id := ctx.Request().Route("commentId")

	var comment models.Comment
	if err := facades.Orm().Query().Where("id", id).First(&comment); err != nil || comment.ID == 0 {
		return helpers.Error(ctx, http.StatusNotFound, "Comment not found", nil)
	}

	if comment.UserID != helpers.AuthUserID(ctx) {
		return helpers.Error(ctx, http.StatusForbidden, "You are not allowed to delete this comment", nil)
	}

	if _, err := facades.Orm().Query().Delete(&comment); err != nil {
		return helpers.Error(ctx, http.StatusBadRequest, "Failed to delete comment", err.Error())
	}

	return helpers.Success(ctx, http.StatusOK, "Comment deleted successfully", nil)
}

Perhatikan di Index dan Store, kita cek dulu apakah post-nya ada sebelum lanjut memproses komentar — supaya user tidak bisa berkomentar di post yang sudah dihapus atau memang tidak pernah ada. Pola proteksi kepemilikan di Destroy juga sama persis dengan yang kita pakai di PostController pada Part 11: komentar hanya bisa dihapus oleh yang membuatnya.

Menambahkan Route

Endpoint list komentar sifatnya publik, tapi menambah dan menghapus komentar butuh login:

commentController := controllers.NewCommentController()
facades.Route().Get("/posts/{id}/comments", commentController.Index)

facades.Route().Middleware(middleware.JwtMiddleware()).Group(func(router route.Router) {
	router.Post("/posts/{id}/comments", commentController.Store)
	router.Delete("/comments/{commentId}", commentController.Destroy)
})

Testing

  1. Login untuk mendapatkan token.
  2. POST /posts/{id}/comments dengan body {"body": "..."} dan header Authorization: Bearer {token}.
  3. GET /posts/{id}/comments — bisa diakses tanpa login, akan menampilkan daftar komentar beserta data user yang berkomentar.
  4. DELETE /comments/{commentId} — coba pakai token user lain untuk memastikan dapat response 403 Forbidden.

Penutup

Fitur Comment ini melengkapi interaksi dasar di aplikasi blog kita. Di part selanjutnya kita akan menambahkan Kategori dan Tag untuk post, supaya konten bisa dikelompokkan dan lebih mudah dicari.

M
Mr Sugiarto

Developer

Bagian dari Series: Seri Tutorial Belajar Framework Goravel Rest API untuk Pemula

Overview Seri ini membahas cara membangun REST API menggunakan Goravel, framework web berbasis Golang yang terinspirasi dari Laravel. Materi mencakup...

Lihat Series Lengkap