Xây dựng server RESTful API với Golang sử dụng gin-gonic framework


  • Trùm cuối

    2f52681f-0cd0-4e62-a4b5-c2de0e32556b-image.png

    RESTful API là gì?

    RESTful API là một tiêu chuẩn dùng trong việc thết kế các thiết kế API cho các ứng dụng web để quản lý các resource. RESTful là một trong những kiểu thiết kế API được sử dụng phổ biến nhất ngày nay.

    Trọng tâm của REST quy định cách sử dụng các HTTP method (như GET, POST, PUT, DELETE...) và cách định dạng các URL cho ứng dụng web để quản các resource. Ví dụ với một trang blog để quản lý các bài viết chúng ta có các URL đi với HTTP method như sau:

    Với các ứng dụng web được thiết kế sử dụng RESTful, lập trình viên có thể dễ dàng biết được URL và HTTP method để quản lý một resource. Bạn cũng cần lưu ý bản thân RESTful không quy định logic code ứng dụng và RESTful cũng không giới hạn bởi ngôn ngữ lập trình ứng dụng. Bất kỳ ngôn ngữ lập trình (hoặc framework) nào cũng có thể áp dụng RESTful trong việc thiết kế API cho ứng dụng web.

    RESTful API server với gin-gonic

    Hôm nay mình sẽ xây dựng một server RESTful API đơn giản cho ứng dụng todo với Golang. Mình sẽ sử dụng framework được đánh giá là đơn giản nhất và nhanh nhất hiện nay là gin-gonic, gọi tắt là gin và sử dụng ORM (Object Relational Mapping) gorm cho database. Để cài đặt các packages này vào workspace $GOPATH/src của bạn, hãy chạy các lệnh sau:

    $ go get gopkg.in/gin-gonic/gin.v1
    $ go get -u github.com/jinzhu/gorm
    $ go get github.com/go-sql-driver/mysql
    

    Nhìn chung trong các ứng dụng CRUD sẽ có các APIs sau:

    • POST todos/
    • GET todos/
    • GET todos/{id}
    • PUT todos/{id}
    • DELETE todos/{id}

    Bắt đầu code thôi nào các bác, đến thư mục $GOPATH/src và tạo folder todo. Bên trong folder todo tạo file main.go. Import “gin framework” và project và tạo các routes như bên dưới trong hàm main. Nếu các bạn muốn thêm prefix cho API chẳng hạn api/v1, chúng ta hãy sử dụng phương thức router Group.

    import (
           "github.com/gin-gonic/gin"
    )
    func main() {
    router := gin.Default()
    v1 := router.Group("/api/v1/todos")
     {
      v1.POST("/", createTodo)
      v1.GET("/", fetchAllTodo)
      v1.GET("/:id", fetchSingleTodo)
      v1.PUT("/:id", updateTodo)
      v1.DELETE("/:id", deleteTodo)
     }
     router.Run()
    }
    

    Chúng ta đã tạo được 5 routes và chúng được sử dụng để xử lý một số chức năng như createTodo, fetchAllTodo... Chúng ta sẽ thảo luận về nó sau.

    Bây giờ chúng ta cần thiét lập kết nối database. Để sử dụng database chúng ta pull gorm package và mysql dialects về. Xem hướng dẫn dưới đây:

    package main
    
    import (
           "github.com/gin-gonic/gin"
           "github.com/jinzhu/gorm"
           _ "github.com/jinzhu/gorm/dialects/mysql"
    )
    
    var db *gorm.DB
    func init() {
     //open a db connection
     var err error
     db, err = gorm.Open("mysql", "root:12345@/demo?charset=utf8&parseTime=True&loc=Local")
     if err != nil {
      panic("failed to connect database")
     }
    //Migrate the schema
     db.AutoMigrate(&todoModel{})
    }
    

    Trong đoạn code ở trên mysql là database driver, root là database username, 12345passworddemo chính là tên của database. Hãy thay đổi những tham số này cho phù hợp.

    Chúng ta sẽ sử dụng hàm Database để lấy kết nối database. Hãy tạo một todoModeltransformedTodo struct. Struct đầu tiên sẽ đại diện cho Todo gốc và struct thứ 2 sẽ giữ giá trị todo được điều chỉnh để trả về trong response của API. Chúng ta phải thực hiện điều chỉnh dữ liệu trả về bởi chắc chắn là các bạn không muốn trả về một số fields dư thừa như updated_at, created_at... rồi.

    type (
     // todoModel describes a todoModel type
     todoModel struct {
      gorm.Model
      Title     string `json:"title"`
      Completed int    `json:"completed"`
     }
    // transformedTodo represents a formatted todo
     transformedTodo struct {
      ID        uint   `json:"id"`
      Title     string `json:"title"`
      Completed bool   `json:"completed"`
     }
    )
    

    Struct Todo có một số trường mở rộng như gorm.Model, ý nghĩa của nó là gì? Vâng, trường này sẽ nhúng một Model struct cho chúng ta bao gồm 4 trường mở rộng ID, CreatedAt, UpdatedAt, DeletedAt.

    Gorm có migration, chúng ta sẽ sử dụng nó trong hàm init. Khi chúng ta chạy ứng dụng, đấu tiên nó sẽ tạo một kết nối đến CSDL và thực hiện migrate.

    //Migrate the schema
     db.AutoMigrate(&todoModel{})
    

    31473de8-b8dc-48d0-8cd1-f592ef43ece7-image.png

    Bạn còn nhớ 5 routes mà chúng ta đã định nghĩa ở trên không? Hãy code 5 phương thức cho mỗi routes này từng cái một.

    Khi một user gửi một POST request đến đường dẫn api/v1/todos với titlecompleted chúng sẽ được xử lý bởi route này v1.POST("/", createTodo)

    Hãy code hàm createTodo:

    // createTodo add a new todo
    func createTodo(c *gin.Context) {
     completed, _ := strconv.Atoi(c.PostForm("completed"))
     todo := todoModel{Title: c.PostForm("title"), Completed: completed}
     db.Save(&todo)
     c.JSON(http.StatusCreated, gin.H{"status": http.StatusCreated, "message": "Todo item created successfully!", "resourceId": todo.ID})
    }
    

    Ở đoạn code trên, chúng ta sử dụng gin Context để nhận dữ liệu được gửi lên và gorm sẽ giúp lưu dữ liệu todo này vào database. Sau khi lưu trữ dữ liệu xong chúng ta sẽ trả về id cho user.

    Hãy tiếp tục với các hàm còn lại:

    // fetchAllTodo fetch all todos
    func fetchAllTodo(c *gin.Context) {
     var todos []todoModel
     var _todos []transformedTodo
    db.Find(&todos)
    if len(todos) <= 0 {
      c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
      return
     }
    //transforms the todos for building a good response
     for _, item := range todos {
      completed := false
      if item.Completed == 1 {
       completed = true
      } else {
       completed = false
      }
      _todos = append(_todos, transformedTodo{ID: item.ID, Title: item.Title, Completed: completed})
     }
     c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todos})
    }
    // fetchSingleTodo fetch a single todo
    func fetchSingleTodo(c *gin.Context) {
     var todo todoModel
     todoID := c.Param("id")
    db.First(&todo, todoID)
    if todo.ID == 0 {
      c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
      return
     }
    completed := false
     if todo.Completed == 1 {
      completed = true
     } else {
      completed = false
     }
    _todo := transformedTodo{ID: todo.ID, Title: todo.Title, Completed: completed}
     c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todo})
    }
    // updateTodo update a todo
    func updateTodo(c *gin.Context) {
     var todo todoModel
     todoID := c.Param("id")
    db.First(&todo, todoID)
    if todo.ID == 0 {
      c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
      return
     }
    db.Model(&todo).Update("title", c.PostForm("title"))
     completed, _ := strconv.Atoi(c.PostForm("completed"))
     db.Model(&todo).Update("completed", completed)
     c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo updated successfully!"})
    }
    // deleteTodo remove a todo
    func deleteTodo(c *gin.Context) {
     var todo todoModel
     todoID := c.Param("id")
    db.First(&todo, todoID)
    if todo.ID == 0 {
      c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
      return
     }
    db.Delete(&todo)
     c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo deleted successfully!"})
    }
    

    Trong hàm fetchAllTodo chũng ta đã lấy danh sách tất cả todos và xây dựng một response trả về với id, titlecompleted. Chúng ta đã bỏ qua các trường CreatedAt, UpdatedAt, DeletedAt và ép kiểu integer sang boolean.

    Vâng, chúng ta đã code đủ hàm để test rùi, bắt tay vào xây dựng app thôi nào. Mình sẽ test API server này sử dụng Postman (các bạn cũng có thể dùng CURL).

    Build ứng dụng bằng terminal và chạy thử thôi nào:

    $ go build main.go
    

    Lệnh này sẽ build ra một file binary main sau đó chúng ta chạy chương trình bằng lệnh $ ./main. Quáo, app cùi của chúng ta đã chạy trên port 8080. Trên terminal bây giờ sẽ hiện log của server để các bạn dễ theo dõi.

    Mình sẽ sử dụng Postman để test

    787be86f-51c1-4f56-94d1-0818d3a97fd6-image.png
    Tạo một todo

    0a3d1f49-02d3-4364-a40e-1cf0b3710357-image.png
    Fetch tất cả todos

    60337446-c732-4573-8225-c9ceff508de5-image.png
    Fetch một todo với id

    934847be-5e46-4651-99e0-0b603e940f62-image.png
    Update một todo

    c98dc4b2-071f-4ee6-8ef5-a6195b2b5454-image.png
    Xoá một todo

    Dưới đây là mã nguồn đầy đủ của chương trình:

    package main
    
    import (
    	"net/http"
    	"strconv"
    
    	"github.com/gin-gonic/gin"
    	"github.com/jinzhu/gorm"
    	_ "github.com/jinzhu/gorm/dialects/mysql"
    )
    
    var db *gorm.DB
    
    func init() {
    	//open a db connection
    	var err error
    	db, err = gorm.Open("mysql", "root:12345@/demo?charset=utf8&parseTime=True&loc=Local")
    	if err != nil {
    		panic("failed to connect database")
    	}
    
    	//Migrate the schema
    	db.AutoMigrate(&todoModel{})
    }
    
    func main() {
    
    	router := gin.Default()
    
    	v1 := router.Group("/api/v1/todos")
    	{
    		v1.POST("/", createTodo)
    		v1.GET("/", fetchAllTodo)
    		v1.GET("/:id", fetchSingleTodo)
    		v1.PUT("/:id", updateTodo)
    		v1.DELETE("/:id", deleteTodo)
    	}
    	router.Run()
    
    }
    
    type (
    	// todoModel describes a todoModel type
    	todoModel struct {
    		gorm.Model
    		Title     string `json:"title"`
    		Completed int    `json:"completed"`
    	}
    
    	// transformedTodo represents a formatted todo
    	transformedTodo struct {
    		ID        uint   `json:"id"`
    		Title     string `json:"title"`
    		Completed bool   `json:"completed"`
    	}
    )
    
    // createTodo add a new todo
    func createTodo(c *gin.Context) {
    	completed, _ := strconv.Atoi(c.PostForm("completed"))
    	todo := todoModel{Title: c.PostForm("title"), Completed: completed}
    	db.Save(&todo)
    	c.JSON(http.StatusCreated, gin.H{"status": http.StatusCreated, "message": "Todo item created successfully!", "resourceId": todo.ID})
    }
    
    // fetchAllTodo fetch all todos
    func fetchAllTodo(c *gin.Context) {
    	var todos []todoModel
    	var _todos []transformedTodo
    
    	db.Find(&todos)
    
    	if len(todos) <= 0 {
    		c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
    		return
    	}
    
    	//transforms the todos for building a good response
    	for _, item := range todos {
    		completed := false
    		if item.Completed == 1 {
    			completed = true
    		} else {
    			completed = false
    		}
    		_todos = append(_todos, transformedTodo{ID: item.ID, Title: item.Title, Completed: completed})
    	}
    	c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todos})
    }
    
    // fetchSingleTodo fetch a single todo
    func fetchSingleTodo(c *gin.Context) {
    	var todo todoModel
    	todoID := c.Param("id")
    
    	db.First(&todo, todoID)
    
    	if todo.ID == 0 {
    		c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
    		return
    	}
    
    	completed := false
    	if todo.Completed == 1 {
    		completed = true
    	} else {
    		completed = false
    	}
    
    	_todo := transformedTodo{ID: todo.ID, Title: todo.Title, Completed: completed}
    	c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todo})
    }
    
    // updateTodo update a todo
    func updateTodo(c *gin.Context) {
    	var todo todoModel
    	todoID := c.Param("id")
    
    	db.First(&todo, todoID)
    
    	if todo.ID == 0 {
    		c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
    		return
    	}
    
    	db.Model(&todo).Update("title", c.PostForm("title"))
    	completed, _ := strconv.Atoi(c.PostForm("completed"))
    	db.Model(&todo).Update("completed", completed)
    	c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo updated successfully!"})
    }
    
    // deleteTodo remove a todo
    func deleteTodo(c *gin.Context) {
    	var todo todoModel
    	todoID := c.Param("id")
    
    	db.First(&todo, todoID)
    
    	if todo.ID == 0 {
    		c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
    		return
    	}
    
    	db.Delete(&todo)
    	c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo deleted successfully!"})
    }
    

    Lưu ý: Khi bạn triển khai lên production cần phải chú ý những bước sau:

    • Không fetch tất cả dữ liệu với select * from todos, hãy sử dụng phân trang.
    • Không bao giờ phó mặc cho user input. Hãy validate nó, có một số packages trong Go hỗ trợ việc này.
    • Kiểm tra hết những lỗi có thể xảy ra
    • Bạn nên sử dụng loggingauthentication

    Chúc các bạn thành công (hoặc thành thụ 😂)!

    good luck



Có thể bạn cũng quan tâm

.
DMCA.com Protection Status