Site icon JocoDEV

Membangun RESTful API dengan Go dan Gin

Pada tutorial ini saya akan menggunakan framework Gin untuk membuat layanan RESTful API yang akan menyederhanakan tugas pemrograman yang terkait dengan pembuatan aplikasi web, termasuk layanan web. Gin akan mengarahkan permintaan, mengambil detail permintaan, dan memberikan respon berupa JSON atau format lainnya.

Saya akan menggunakan data film sebagai contoh, dan API yang dibuat bertugas untuk menambah, mengedit, menghapus, maupun mengambil data.

Sebagai prasyarat untuk menyelesaikan proyek ini, kita akan membutuhkan:

Merancang Spesifikasi API

Sebelum menentukan jenis endpoin yang harus disediakan, kita dapat menentukan berdasarkan fungsi yang diharapkan, di antaranya melihat semua data, satu data berdasarkan ID, menambah, mengedit dan menghapus data. Untuk semua kebutuhan itu, kita harus menyediakan beberapa endpoin yang bisa diakses oleh pengguna. Di tutorial ini belum diterapkan otentikasi supaya kode program tetap sederhana. Semua data yang disediakan sebagai keluaran akan berupa JSON.

Untuk memenuhi semua kebutuhan tadi kita sediakan beberapa endpoin, di antaranya:

/films

/film/:id

/film

/film/:id

Selanjutnya kita mulai membuat kode programnya.

Persiapkan Folder untuk Kode Program

Untuk memulai, buat sebuah proyek untuk kode program yang akan ditulis.

  1. Buka terminal dan masuk ke home direktori atau direktori yang diinginkan untuk pengembangan semua aplikasi.
    Pada Linux atau Mac:
    $ cd
    Pada Windows:
    C:\> cd %HOMEPATH%
  2. Masih melalui terminal, buat sebuah direktori untuk kode program sebut saja api-gin.
    $ mkdir api-gin
    $ cd api-gin
  3. Buat sebuah modul yang akan mengatur ketergantungan.
    Jalankan perintah go mod init pada direktori api-gin, berikan path dari modul dimana kode program akan berada di dalamnya.
    $ go mod init jocodev/api-gin
    go: creating new go.mod: module jocodev/api-gin

    Perintah di atas akan membuat file go.mod dimana ketergantungan akan dimasukkan ke dalamnya untuk keperluan pelacakan.

Persiapkan Data

Supaya kode program tetap sederhana, semua data akan disimpan dalam memori, meski secara umum data akan disimpan dalam database. Hal ini akan menyebabkan data selalu kembali ke semula saat kita menjalankan ulang kode programnya.

Gunakan editor teks dan buat file main.go di dalam direktori api-gin yang telah dibuat sebelumnya. Kita akan menulis kode program di dalam file tersebut.

Di dalam main.go, pada bagian atas, tulislah kode berikut:

package main

Kode program mandiri selalu menggunakan package main.

Di bawahnya salinlah deklarasi berikut yang merupakan film struct. Kita akan menyimpan data film di dalam memori. Label struct seperti json:”kategori” menentukan nama field yang seharusnya saat diserialkan menjadi JSON. Tanpanya, JSON yang terbentuk akan mengikuti label utama dengan huruf kapital di depannya.

// film struktur yang mewakili data sebuah film.
type film struct {
	ID       string `json:"id"`
	Judul    string `json:"judul"`
	Kategori string `json:"kategori"`
}

Di bawahnya salin data berikut yang mengikuti struct dari film sebelumnya.

// films data.
var films = map[string]film{
	"1": {ID: "1", Judul: "Gods of Egypt", Kategori: "Laga & Petualangan"},
	"2": {ID: "2", Judul: "Spider-man Homecoming", Kategori: "Laga & Petualangan"},
	"3": {ID: "3", Judul: "Now You See Me", Kategori: "Kriminal"},
}

Mengunduh Modul Gin

Sebelumnya, kita unduh terlebih dulu modul Gin dengan perintah go get:

go get github.com/gin-gonic/gin

Pada file go.mod akan terlihat seperti ini:

module jocodev/api-gin

go 1.16

require github.com/gin-gonic/gin v1.7.7

Menangani Permintaan Pengguna

Untuk menangani EndPoint /films dengan metode GET, salin kode program berikut:

// getFilms merespon dengan semua data film yang ada.
func getFilms(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, films)
}

Pada kode program di atas, kita membuat sebuah fungsi dengan nama getFilms yang akan mengambil parameter gin.Context, gin.Context sendiri merupakan bagian terpenting dari Gin karena membawa detail permintaan, memvalidasi, membuat serial JSON, dan masih banyak lagi. Selanjutnya memanggil fungsi Context.IndentedJSON yang akan merubah objek menjadi JSON yang dimasukkan ke dalam body respon, pada argumen pertama terdapat kode status HTTP yang dikirim ke klien. Di sini kita menggunakan http.StatusOK yang merupakan sebuah konstanta dari paket net/http yang mengindikasikan kode status 200 OK.

Apabila kita telah menginstalasi Go pada Visual Studio Code, secara otomatis pada bagian import akan muncul baris kode berikut:

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

Paket net/http merupakan modul bawaan Go, sehingga tanpa perlu kita unduh otomatis akan muncul saat kita mengunakannya di dalam kode program. Begitu juga dengan yang telah kita unduh dengan perintah go get seperti gin. Tanpa perlu ditambahkan ke dalam blok import, modul akan muncul dengan sendirinya.

Selanjutnya buat fungsi main yang akan dieksekusi saat kode program dijalankan, salin kode program berikut:

func main() {
	router := gin.Default()
	router.GET("/films", getFilms)

	router.Run("localhost:8080")
}

Pada kode program ini kita menginisialisasi router Gin menggunakan Default, gunakan fungsi GET dengan endpoint /films sebagai argumen pertama dan fungsi getFilms sebagai argumen kedua. Fungsi Run akan menggunakan semua metode yang telah ditetapkan sebelumnya ke dalam http.Server untuk mulai menjalankan server.

Kita uji apakah endpoint /films bekerja sesuai harapan, jalankan kode program dengan perintah:

go run .

Pada bagian akhir seharusnya akan muncul pesan berikut:

[GIN-debug] Listening and serving HTTP on localhost:8080

Ini menandakan server sudah berjalan, dan bisa kita uji dengan perangkat CURL, jalankan perintah berikut pada terminal:

curl http://localhost:8080/films

Seharusnya akan muncul respon di bawahnya seperti berikut:

{
    "1": {
        "id": "1",
        "judul": "Gods of Egypt",
        "kategori": "Laga \u0026 Petualangan"
    },
    "2": {
        "id": "2",
        "judul": "Spider-man Homecoming",
        "kategori": "Laga \u0026 Petualangan"
    },
    "3": {
        "id": "3",
        "judul": "Now You See Me",
        "kategori": "Kriminal"
    }
}

Selanjutnya kita tangani permintaan data film berdasarkan ID-nya dengan endpoint /film/:id, salin fungsi getFilmByID berikut:

// getFilmByID merespon dengan data film berdasarkan ID.
func getFilmByID(c *gin.Context) {
	id := c.Param("id")

	if f, exists := films[id]; exists {
		c.IndentedJSON(http.StatusOK, f)
		return
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "film tidak ditemukan"})
}

Fungsi di atas agak berbeda dengan sebelumnya, kali ini kita menganalisa parameter yang dikirim oleh klien, yaitu c.Param(“id”). Selanjutnya menyaring data film dengan melacak apakah data film memiliki key dengan ID yang dikirim dari klien, maka kembalikan nilai f sebagai JSON. Jika tidak ditemukan, kode status http yang dikirim ke klien adalah http.StatusNotFound atau error 404.

Tambahkan router GET dengan argumen pertama berisi string /film/:id pada kode program, secara lengkap fungsi main akan seperti berikut:

func main() {
	router := gin.Default()
	router.GET("/films", getFilms)
	router.GET("/film/:id", getFilmByID)

	router.Run("localhost:8080")
}

Matikan server dan jalankan ulang dengan perintah go run . untuk mengimplementasi perubahan. Selanjutnya jalankan perintah curl berikut:

curl http://localhost:8080/film/2

Seharusnya akan muncul data berikut:

{
    "id": "2",
    "judul": "Spider-man Homecoming",
    "kategori": "Laga \u0026 Petualangan"
}

Menangani Permintaan Manipulasi Data

Idealnya permintaan ini dibatasi dengan otentikasi tertentu supaya tidak semua pengguna dapat memanipulasi data secara bebas, namun seperti yang saya bahas sebelumnya kita tidak akan menerapkan otentikasi pada tutorial ini.

Untuk menangani endpoint POST /film kita buat fungsi addFilm, salin kode program berikut:

// addFilm menambah data film
func addFilm(c *gin.Context) {
	var newFilm film
	if err := c.ShouldBindJSON(&newFilm); err != nil {
		c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "data tidak valid"})
		return
	}
	if _, exists := films[newFilm.ID]; exists {
		c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "data sudah ada!"})
		return
	}
	films[newFilm.ID] = newFilm
	c.IndentedJSON(http.StatusCreated, newFilm)
}

Pada awal fungsi buat variabel baru newFilm dengan struct film, ambil data body JSON dari pengguna dengan fungsi ShouldBindJSON dan dimasukkan ke dalam variabel newFilm, apabila terdapat kesalahan, respon dengan kode status StatusBadRequest. Dan apabila ID yang dimasukkan sama dengan data yang sudah ada maka respon dengan pesan “data sudah ada!”. Ketika tidak ada masalah, respon dengan kode status StatusCreated dengan body newFilm.

Tambahkan router POST /film di dalam fungsi main, secara lengkap fungsi main akan seperti berikut:

func main() {
	router := gin.Default()
	router.GET("/films", getFilms)
	router.GET("/film/:id", getFilmByID)

	router.POST("/film", addFilm)

	router.Run("localhost:8080")
}

Matikan server dan jalankan ulang dengan perintah go run . untuk mengimplementasi perubahan. Selanjutnya jalankan perintah curl berikut:

curl http://localhost:8080/film \
 --include \
 --header "Content-Type: application/json" \
 --request "POST" \
 --data '{"id":"4","judul":"Batman Begins","kategori":"Laga"}'

Perintah curl di atas mengakses url http://localhost:8080/film dengan header Content-Type berupa application/json dan metode nya POST. Respon dari perintah di atas seharusnya akan seperti di bawah ini:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Wed, 06 Apr 2022 05:49:04 GMT
Content-Length: 71

{
    "id": "4",
    "judul": "Batman Begins",
    "kategori": "Laga"
}

Respon di atas menandakan tambahan data berhasil dikirim, apabila kita mengulangi perintah curl yang sama, seharusnya akan muncul respon:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Wed, 06 Apr 2022 05:49:40 GMT
Content-Length: 36

{
    "message": "data sudah ada!"
}

Untuk menangani endpoint PATCH /film/:id kita buat fungsi editFilm, salin kode program berikut:

// editFilm mengedit data film berdasarkan ID
func editFilm(c *gin.Context) {
	id := c.Param("id")
	if _, exists := films[id]; exists {
		var newFilm film
		if err := c.ShouldBindJSON(&newFilm); err != nil {
			c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "data tidak valid"})
			return
		}
		newFilm.ID = id
		films[newFilm.ID] = newFilm
		c.IndentedJSON(http.StatusCreated, newFilm)
		return
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "film tidak ditemukan"})
}

Fungsi di atas merupakan gabungan antara endpoint GET /film/:id dan POST /film, yang akan mengecek apakah terdapat data film dengan key id, kemudian body yang dikirim harus mempunyai struktur JSON film. Jika kedua penyaringan terverfikasi, maka data film dengan key id akan diubah menggunakan data baru.

Tambahkan router PATCH /film/:id di dalam fungsi main, secara lengkap fungsi main akan seperti berikut:

func main() {
	router := gin.Default()
	router.GET("/films", getFilms)
	router.GET("/film/:id", getFilmByID)

	router.POST("/film", addFilm)
	router.PATCH("/film/:id", editFilm)

	router.Run("localhost:8080")
}

Restart server dan jalankan perintah curl berikut:

curl http://localhost:8080/film/4 \
 --include \
 --header "Content-Type: application/json" \
 --request "PATCH" \
 --data '{"judul":"Batman Begins","kategori":"Kriminal"}'

Seharusnya respon yang didapat akan seperti berikut:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Wed, 06 Apr 2022 06:25:56 GMT
Content-Length: 71

{
    "id": "4",
    "judul": "Batman Begins",
    "kategori": "Aksi"
}

Untuk menangani endpoint DELETE /film/:id kita buat fungsi delFilm, salin kode program berikut:

// delFilm menghapus data film berdasarkan ID
func delFilm(c *gin.Context) {
	id := c.Param("id")
	if _, exists := films[id]; exists {
		delete(films, id)
		c.IndentedJSON(http.StatusCreated, gin.H{"message": "Data berhasil dihapus"})
		return
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "film tidak ditemukan"})
}

Tambahkan router DELETE /film/:id di dalam fungsi main, secara lengkap fungsi main akan seperti berikut:

func main() {
	router := gin.Default()
	router.GET("/films", getFilms)
	router.GET("/film/:id", getFilmByID)

	router.POST("/film", addFilm)
	router.PATCH("/film/:id", editFilm)
	router.DELETE("/film/:id", delFilm)

	router.Run("localhost:8080")
}

Restart server dan jalankan perintah curl berikut:

curl http://localhost:8080/film/4 \
 --include \
 --request "DELETE"

Seharusnya respon yang didapat akan seperti berikut:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Wed, 06 Apr 2022 06:35:20 GMT
Content-Length: 42

{
    "message": "Data berhasil dihapus"
}

Selamat! Anda sudah menyelesaikan proyek RESTful API sederhana dengan menggunakan Go dan Gin. Kode program selengkapnya:

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

// film struktur yang mewakili data sebuah film.
type film struct {
	ID       string `json:"id"`
	Judul    string `json:"judul"`
	Kategori string `json:"kategori"`
}

// films data.
var films = map[string]film{
	"1": {ID: "1", Judul: "Gods of Egypt", Kategori: "Laga & Petualangan"},
	"2": {ID: "2", Judul: "Spider-man Homecoming", Kategori: "Laga & Petualangan"},
	"3": {ID: "3", Judul: "Now You See Me", Kategori: "Kriminal"},
}

// getFilms merespon dengan semua data film yang ada.
func getFilms(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, films)
}

// getFilmByID merespon dengan data film berdasarkan ID.
func getFilmByID(c *gin.Context) {
	id := c.Param("id")
	if f, exists := films[id]; exists {
		c.IndentedJSON(http.StatusOK, f)
		return
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "film tidak ditemukan"})
}

// addFilm menambah data film
func addFilm(c *gin.Context) {
	var newFilm film
	if err := c.ShouldBindJSON(&newFilm); err != nil {
		c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "data tidak valid"})
		return
	}
	if _, exists := films[newFilm.ID]; exists {
		c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "data sudah ada!"})
		return
	}
	films[newFilm.ID] = newFilm
	c.IndentedJSON(http.StatusCreated, newFilm)
}

// editFilm mengedit data film berdasarkan ID
func editFilm(c *gin.Context) {
	id := c.Param("id")
	if _, exists := films[id]; exists {
		var newFilm film
		if err := c.ShouldBindJSON(&newFilm); err != nil {
			c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "data tidak valid"})
			return
		}
		newFilm.ID = id
		films[newFilm.ID] = newFilm
		c.IndentedJSON(http.StatusCreated, newFilm)
		return
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "film tidak ditemukan"})
}

// delFilm menghapus data film berdasarkan ID
func delFilm(c *gin.Context) {
	id := c.Param("id")
	if _, exists := films[id]; exists {
		delete(films, id)
		c.IndentedJSON(http.StatusCreated, gin.H{"message": "Data berhasil dihapus"})
		return
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "film tidak ditemukan"})
}

func main() {
	router := gin.Default()
	router.GET("/films", getFilms)
	router.GET("/film/:id", getFilmByID)

	router.POST("/film", addFilm)
	router.PATCH("/film/:id", editFilm)
	router.DELETE("/film/:id", delFilm)

	router.Run("localhost:8080")
}
Exit mobile version