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
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
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"
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")
}
}
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"`
}
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"
)
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()
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.
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)
}
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")
}
go run main.go
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.
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 ""
}
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.
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.
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
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
}
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 telah menyiapkan 2 endpoint pada controller post.go.
Buka main.go tambahkan baris berikut.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)
...
}
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 :
Kalo ga jalan kemungkinan agan salah copy paste atau kopinya kurang kentl atau saya yang salah nulis, yang udah pasti jalan ada di link di bawah.
Link Github
Referensi
https://www.twilio.com/blog/build-restful-api-using-golang-and-gin