Table of Contents

02 – Membuat RESTful API mengunakan Golang and Gin

Fitur
  1. Registrasi dengan email dan password
  2. Login dengan email dan password
  3. Create new Post (Proteksi JWT)
  4. Get all Post (Proteksi JWT)
Kebutuhan
  1. Pemahaman dasar tentang Golang, Gorm dan JWTs
  2. Postman
  3. Akun Github
  4. Go 1.1x
  5. PostgreSQL/MySQL Server terinstall
Buat Project

Buat folder baru bernama goginapi, lalu inisialisasi Go Module di dalamnya.

				
					mkdir goginapi
cd goginapi
go mod init github.com/mudybang/go-web-examples/goginapi //sesuaikan dengan akun github anda
				
			
Install Dependency

Project ini akan menggunakan fremework Gin. Jalankan perintah berikut untuk menginstal Gin versi terbaru, bersama dengan beberapa dependensi lain yang diperlukan.

				
					go get github.com/gin-gonic/gin github.com/golang-jwt/jwt/v4 github.com/joho/godotenv golang.org/x/crypto gorm.io/driver/postgres gorm.io/gorm
				
			
  1. Go Cryptography: Menyediakan library Go cryptography.
  2. GoDotEnv: untuk menghiduplan penggunaan .env.
  3. GORM: merupakan  salah satu library ORM (Object Relational Mapper) untuk Golang. Selain itu Gorm juga menyediakan driver database PostgreSQL.
  4. JWT-Go: menyediakan fitur JSON Web Tokens.
Menyiapkan Database & Setting Environment

Sebelum memulai project kita akan menyiapkan database pada PostgreSQL bernama blog_db

				
					createdb -h <DB_HOSTNAME> -p <DB_PORT> -U <DB_USER> blog_db --password
				
			

Lanjut setting konfigurasi database di file .env, buat dulu filenya lalu isi konfigurasi seperti di bawah ini.

				
					# Database credentials
DB_HOST="<<DB_HOST>>"
DB_USER="<<DB_USER>>"
DB_PASSWORD="<<DB_PASSWORD>>"
DB_NAME="diary_app"
DB_PORT="<<DB_PORT>>"

# Authentication credentials
TOKEN_TTL="2000"
JWT_PRIVATE_KEY="THIS_IS_NOT_SO_SECRET+YOU_SHOULD_DEFINITELY_CHANGE_IT"
				
			
Menyiapkan Model

Selanjutnya, Anda akan membuat dua model untuk aplikasi: User dan Post. Untuk melakukannya, mulailah dengan membuat folder baru bernama models. Di direktori tersebut, buat file baru bernama user.go, lalu tambahkan kode berikut ke file yang baru dibuat.

				
					package model

import "gorm.io/gorm"

type User struct {
	gorm.Model
	Name     string `gorm:"size:255;not null;" json:"name"`
	Email    string `gorm:"size:255;not null;unique" json:"email"`
	Password string `gorm:"size:255;not null;" json:"-"`
	Posts    []Post
}

				
			

Struktur di atas merupakan struktur dari Gorm.

Dengan memasukan Posts ke dalam User artinya menentukan hubungan satu-ke-banyak antara struct User dan struct Post.

Lanjut buat model/post.go

				
					package model

import "gorm.io/gorm"

type Post struct {
	gorm.Model
	Title   string `gorm:"size:255;not null;unique" json:"title"`
	Content string `gorm:"size:255;not null;" json:"content"`
}

				
			

Buat folder database, lalu tambahkan file posgree.go. Isi dari file ini adalah func untuk melakukan koneksi ke database.

				
					package database

import (
	"fmt"
	"os"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

var Database *gorm.DB

func Connect() {
	var err error
	host := os.Getenv("DB_HOST")
	username := os.Getenv("DB_USER")
	password := os.Getenv("DB_PASSWORD")
	databaseName := os.Getenv("DB_NAME")
	port := os.Getenv("DB_PORT")

	dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Africa/Lagos", host, username, password, databaseName, port)
	Database, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

	if err != nil {
		panic(err)
	} else {
		fmt.Println("Successfully connected to the database")
	}
}

				
			

Func Connect() mengambil variabel .env yang diperlukan untuk mengatur koneksi database dan kemudian membuka koneksi menggunakan driver GORM PostgreSQL.

Setelah persiapan selesai kita akan membuat func utama main.go di folder utama.

				
					package main

import (
	"log"

	"github.com/joho/godotenv"
	"github.com/mudybang/go-web-examples/goginapi/database"
	"github.com/mudybang/go-web-examples/goginapi/model"
)

func main() {
	loadEnv()
	loadDatabase()
}

func loadDatabase() {
	database.Connect()
	database.Database.AutoMigrate(&model.User{})
	database.Database.AutoMigrate(&model.Post{})
}

func loadEnv() {
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatal("Error loading .env file")
	}
}

				
			

func loadEnv() mengambil .env variables guna melakukan koneksi ke database loadDatabase().

func AutoMigrate() dipanggil untuk membuat table User dan Post sesuai strut model(Jika tidak ada sebelumnya).

Registrasi

Sebelum membuat endpoint API, kita buat dulu struct untuk permintaan autentikasi agar sesuai. Pada folder model, buat file baru bernama auth.go dan tambahkan kode berikut ke dalamnya.

				
					package model

type AuthenticationInput struct {
	Email    string `json:"email" binding:"required"`
	Password string `json:"password" binding:"required"`
}

				
			
Struct Method

Selanjutnya tambahkan method merikut ke dalam struct model/user.go

				
					func (user *User) Save() (*User, error) {
    err := database.Database.Create(&user).Error
    if err != nil {
        return &User{}, err
    }
    return user, nil
}

func (user *User) BeforeSave(*gorm.DB) error {
	passwordHash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	user.Password = string(passwordHash)
	user.Email = html.EscapeString(strings.TrimSpace(user.Email))
	return nil
}
				
			

method Save() untuk menambahkan user baru ke database(jika tidak ada error). Sebelum disimpan setiap spasi pada email dihilangkan dan untuk keamanan string password diubah ke bentuk hash.

Jangan lupa menambahkan library berikut pada section import.

				
					import (
    "github.com/mudybang/go-web-examples/goginapi/model" //sesuaikan
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
    "html"
    "strings"
)
				
			
Controller

 

Selanjutnya buat folder bernama controller lalu buat file authentication.go isi dengan kode berikut.

				
					package controllers

import (
	"net/http"

	"github.com/mudybang/go-web-examples/goginapi/model"

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

func Register(context *gin.Context) {
	var input model.AuthenticationInput

	if err := context.ShouldBindJSON(&input); err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	user := model.User{
		Email:    input.Email,
		Password: input.Password,
	}

	savedUser, err := user.Save()

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	context.JSON(http.StatusCreated, gin.H{"user": savedUser})
}

				
			

func Register()

  1. memvalidasi JSON request
  2. membuat user baru
  3. mengembalikan data yang baru di simpan sebagai json response
Login
Struct Method

Selanjutnya tambahkan method ValidatePassword pada model model/user.go

				
					...
func (user *User) ValidatePassword(password string) error {
	return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
}

func FindUserByEmail(email string) (User, error) {
	var user User
	err := database.Database.Where("email=?", email).Find(&user).Error
	if err != nil {
		return User{}, err
	}
	return user, nil
}
				
			

Hash digenerate menggunakan library bcrypt untuk menangani plaintext password yang nantinya akan dibandingkan dengan hash user:password yang terdapat di dalam database. Error akan ditampilan jika persamaan gagal.

Fungsi FindUserByEmail juga dibuat untuk menangani query berdasarkan email untuk mendapatkan user yang dituju.

Json Web Token

Buat folder baru bernama helper, lalu buat file jwt.go dengan isi berikut.

				
					package helper

import (
    "github.com/mudybang/go-web-examples/goginapi/model"
    "github.com/golang-jwt/jwt/v4"
    "os"
    "strconv"
    "time"
)

var privateKey = []byte(os.Getenv("JWT_PRIVATE_KEY"))

func GenerateJWT(user model.User) (string, error) {
    tokenTTL, _ := strconv.Atoi(os.Getenv("TOKEN_TTL"))
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "id":  user.ID,
        "iat": time.Now().Unix(),
        "eat": time.Now().Add(time.Second * time.Duration(tokenTTL)).Unix(),
    })
    return token.SignedString(privateKey)
}
				
			
Controller

Tambahkan fungsi berikut pada Controller controller/auth.go

				
					...
func Login(context *gin.Context) {
	var input model.AuthenticationInput

	if err := context.ShouldBindJSON(&input); err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	user, err := model.FindUserByEmail(input.Email)

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	err = user.ValidatePassword(input.Password)

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	jwt, err := helper.GenerateJWT(user)
	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	context.JSON(http.StatusOK, gin.H{"jwt": jwt})
}
				
			

Lalu tambahkan baris berikut pada section import.

				
					import (
	...
	"github.com/mudybang/go-web-examples/goginapi/helper"
    ...
...
				
			

Buka kembali main.go lalu tambahkan dengan fungsi berikut.

				
					import (
    ...
    "github.com/mudybang/go-web-examples/goginapi/controller"
)

func main() {
	...
	serveApplication()
}
...
func serveApplication() {
    router := gin.Default()

    publicRoutes := router.Group("/auth")
    publicRoutes.POST("/register", controller.Register)
    publicRoutes.POST("/login", controller.Login)

    router.Run(":8000")
    fmt.Println("Server running on port 8000")
}
				
			
Test
				
					go run main.go
				
			
Middleware

Pada tahap berikutnya kita akan mengimplementasikan penggunaan Middleware menggunakan JWT dimana saat ada request Middleware akan memastikan bahwa request tersebut memiliki token autentikasi yang valid sebelum memperbolehkannya masuk.

Persiapan

Sebelum membangun middleware, kita perlu menambahkan beberapa fungsi pembantu untuk mempermudah proses ekstrasi dan validasi JWT. Tambahkan fungsi berikut ke helper/jwt.go.

				
					import (
    ...
    "errors"
    "fmt"
    "strings"
    "github.com/gin-gonic/gin"
    ...
)
...
func ValidateJWT(context *gin.Context) error {
    token, err := getToken(context)
    if err != nil {
        return err
    }
    _, ok := token.Claims.(jwt.MapClaims)
    if ok && token.Valid {
        return nil
    }
    return errors.New("invalid token provided")
}

func CurrentUser(context *gin.Context) (model.User, error) {
    err := ValidateJWT(context)
    if err != nil {
        return model.User{}, err
    }
    token, _ := getToken(context)
    claims, _ := token.Claims.(jwt.MapClaims)
    userId := uint(claims["id"].(float64))

    user, err := model.FindUserById(userId)
    if err != nil {
        return model.User{}, err
    }
    return user, nil
}

func getToken(context *gin.Context) (*jwt.Token, error) {
    tokenString := getTokenFromRequest(context)
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }

        return privateKey, nil
    })
    return token, err
}

func getTokenFromRequest(context *gin.Context) string {
    bearerToken := context.Request.Header.Get("Authorization")
    splitToken := strings.Split(bearerToken, " ")
    if len(splitToken) == 2 {
        return splitToken[1]
    }
    return ""
}
				
			
  1. Fungsi getTokenFromRequest() mengambil nilai dari header authentication dan memisahkan string bearer dari valuenya hingga mengembalikan nilai tokennya saja.
  2. Fungsi getToken() mengurai token menggunakan  kunci yang terdapat pada .env.
  3. fungsi ValidateJWT() memastikan bahwa permintaan yang masuk berisi token yang valid.
  4. fungsi CurrentUser() akan digunakan untuk mendapatkan id penguna dari token yang sudah di enkripsi
  5. fungsi FindUserById() yang belum terdefinisi, jadi akan kita tambahkan dulu ke model.

Pada fungsi model/user.go, tambahkan kode berikut.

				
					...
func FindUserById(id uint) (User, error) {
	var user User
	err := database.Database.Preload("Posts").Where("ID=?", id).Find(&user).Error
	if err != nil {
		return User{}, err
	}
	return user, nil
}
				
			

Selain mengambil data User, data Post yang terkait dengan User juga akan dimuat sehingga Posts pada struct User juga ikut terisi.

Buat Middleware

Setelah persiapan selesai, buat folder bernama middleware, lalu buat file bernama jwtAuth.go

				
					package middleware

import (
    "diary_api/helper"
    "github.com/gin-gonic/gin"
    "net/http"
)

func JWTAuthMiddleware() gin.HandlerFunc {
    return func(context *gin.Context) {
        err := helper.ValidateJWT(context)
        if err != nil {
            context.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
            context.Abort()
            return
        }
        context.Next()
    }
}
				
			

JWTAuthMiddleware mengembalikan fungsi Gin HandlerFunc. Fungsi yang biasanya dipakai untuk controller.

Fungsi ini memanggil fungsi ValidateJWT. Jika tidak valid, respons kesalahan dikembalikan. Jika tidak, fungsi Next() pada context akan dipanggil.

Next() yaitu controller yang ditentukan oleh route.

Menyiapkan endpoint Post

Sebelum menerapkan middleware pada route ataupun controller, terlebih dulu kita siapkan controller dan method untuk route Post.

Tambahkan baris berikut pada model model/post.go

Struct Method
				
					import (
	"github.com/mudybang/go-web-examples/goginapi/database"
	...
)
...
func (post *Post) Save() (*Post, error) {
	err := database.Database.Create(&post).Error
	if err != nil {
		return &Post{}, err
	}
	return post, nil
}
				
			
Controller

Pada folder controller, buat file bernama controller/post.go

				
					package controller

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/mudybang/go-web-examples/goginapi/helper"
	"github.com/mudybang/go-web-examples/goginapi/model"
)

func GetAllPosts(context *gin.Context) {
	user, err := helper.CurrentUser(context)

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	context.JSON(http.StatusOK, gin.H{"data": user.Posts})
}

func CreatePost(context *gin.Context) {
	var input model.Post
	if err := context.ShouldBindJSON(&input); err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	user, err := helper.CurrentUser(context)

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	input.UserID = user.ID

	savedPost, err := input.Save()

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	context.JSON(http.StatusCreated, gin.H{"data": savedPost})
}

				
			

Kita menyiapkan 2 endpoint pada controller post.go.

  1. GetAllPosts(), untuk memberikan output berupa semua data posts yang dimiliki oleh User.
  2. CreatePost(), untuk menambah data baru pada table posts
 
Menerapkan Middleware pada Route

Buka main.go tambahkan baris berikut.

				
					import (
	...
	"github.com/mudybang/go-web-examples/goginapi/middleware"
	...
)

func serveApplication() {
	...
	protectedRoutes := router.Group("/api")
	protectedRoutes.Use(middleware.JWTAuthMiddleware())
	protectedRoutes.POST("/post", controller.CreatePost)
	protectedRoutes.GET("/posts", controller.GetAllPosts)
    ...
}
				
			
Test API

Sebelumnya kita telah mengetest API untuk register & login.

Berikut ini hasil test untuk mendapatkan data posts dan membuat data post baru, dengan tahapan sebagai berikut :

  1. POST halaman auth/login, untuk mendapatkan token
  2. POST halaman “api/post”, menggunakan token
  3. GET halaman “api/posts”, menggunakan token
POST auth/login
POST api/post
GET api/posts

Kalo ga jalan kemungkinan agan salah copy paste atau saya yang salah nulis, yang udah pasti jalan ada di link di bawah.

Link Github

https://github.com

Referensi

https://www.twilio.com/blog/build-restful-api-using-golang-and-gin