Skip to content

03. Week 3 RESTful API

Tran Phong Phu edited this page Mar 18, 2019 · 5 revisions

Working with RESTful API, Gin, Gorm, MySQL

Today we are going to build a simple API for note application with the golang programming language. We are going to use golang simplest/fastest framework gin-gonic and a beautiful ORM gorm for our database work. To install these packages go to your workspace $GOPATH/src and run these command below:

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

In generic CRUD application we need the API's as follows:

  1. POST note/
  2. GET note/
  3. GET note/{id}
  4. PUT note/{id}
  5. DELETE note/{id}

Let's start coding, go to your $GOPATH/src and make a directory note. Inside the note directory create a file main.go. Import the gin framework to our project and create the routes like below inside main function. I like to add a prefix of the apis like api/v1/, that's why we'll use the router Group method

package main

import (
  "github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
v1 := router.Group("/api/v1/notes")
 {
  v1.POST("/", createNote)
  v1.GET("/", fetchAllNote)
  v1.GET("/:id", fetchSingleNote)
  v1.PUT("/:id", updateNote)
  v1.DELETE("/:id", deleteNote)
 }
 router.Run()
}

We have created five routes and they handle some functions like createNote, fetchAllNote etc. We'll discuss about them soon.

Now we need to setup a database connection. To use database pull the gorm package and mysql dialects in our code. Follow the code below:

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", "default:secret@/notes?charset=utf8&parseTime=True&loc=Local")
  if err != nil {
    panic("failed to connect database")
  }
  //Migrate the schema
  db.AutoMigrate(&noteModel{})
}
mysql> DESCRIBE notes;
+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | timestamp        | YES  |     | NULL    |                |
| updated_at | timestamp        | YES  |     | NULL    |                |
| deleted_at | timestamp        | YES  | MUL | NULL    |                |
| title      | varchar(255)     | YES  |     | NULL    |                |
| completed  | tinyint(1)       | YES  |     | NULL    |                |
+------------+------------------+------+-----+---------+----------------+

Notes table after migrated

In the above code mysql is our database driver, default is database username, secret password and notes is database name. Please change these information as your needs.

We'll use the Database function to get the database connection. Lets make a noteModel and transformedNote struct. The first struct will represent the original Note and the second one will hold the transformed note for response to the api. Here we transformed the note response because we don't expose some database fields (updated_at, created_at) to the consumer.

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

Note struct has one field extra gorm.Model what does it mean? well, this field will embed a Model struct for us which contains four fields ID, CreatedAt, UpdatedAt, DeletedAt

Gorm has migration facilities, we already used it in init function. When we run the application first it'll create a connection and then the migration.

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

Can you remember the five routes we wrote a minute earlier? Lets implement the five methods one by one.

When a user send a POST request to the path api/v1/notes/ with title and completed field it'll be handled by this route v1.POST("/", createNote)

Lets Implement the createNote function

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

In the above code we use gin Context to receive the posted data and gorm database connection to save the note. After saving the resource we send the resource id with a good & meaningful response to the user.

Lets implement the rest of the functions

// fetchAllNote fetch all notes
func fetchAllNote(c *gin.Context) {
  var notes []noteModel
  var _notes []transformedNote
  db.Find(&notes)
  if len(notes) <= 0 {
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No note found!"})
    return
  }
  //transforms the notes for building a good response
  for _, item := range notes {
    completed := false
    if item.Completed == 1 {
      completed = true
    } else {
      completed = false
    }
    _notes = append(_notes, transformedNote{ID: item.ID, Title: item.Title, Completed: completed})
  }
  c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _notes})
}
// fetchSingleNote fetch a single note
func fetchSingleNote(c *gin.Context) {
  var note noteModel
  noteID := c.Param("id")
  db.First(&note, noteID)
  if note.ID == 0 {
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No note found!"})
    return
  }
  completed := false
  if note.Completed == 1 {
    completed = true
  } else {
    completed = false
  }
  _note := transformedNote{ID: note.ID, Title: note.Title, Completed: completed}
  c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _note})
}
// updateNote update a note
func updateNote(c *gin.Context) {
  var note noteModel
  noteID := c.Param("id")
  db.First(&note, noteID)
  if note.ID == 0 {
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No note found!"})
    return
  }
  db.Model(&note).Update("title", c.PostForm("title"))
  completed, _ := strconv.Atoi(c.PostForm("completed"))
  db.Model(&note).Update("completed", completed)
  c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Note updated successfully!"})
}
// deleteNote remove a note
func deleteNote(c *gin.Context) {
  var note noteModel
  noteID := c.Param("id")
  db.First(&note, noteID)
  if note.ID == 0 {
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No note found!"})
    return
  }
  db.Delete(&note)
  c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Note deleted successfully!"})
}

In the fetchAllNote function we fetched all the notes and and build a transformed response with id, title, completed. We removed the CreatedAt, UpdatedAt, DeletedAt fields and cast the integer value to bool.

Well, we write enough code, let try to build the app and test it, I'm going test it using chrome extension Postman (you can use any REST client like curl to test).

To build the app open your terminal and go the the project directory

$ go build main.go

The command will build a binary file main and to run the file us this command $ ./main. Wow, our simple note app is running on port: 8080. It'll display the debug log, because by default gin run's in debug mode and port 8080.

To test the api run postman and test the api sequentially

You must to install jq

  • For MacOs: brew install jq
  • For Linux: yum install jq or apt install jq

Create a note

$ curl -X POST   http://localhost:8080/note   -H 'content-type: application/json'   -d '{"title": "ABC", "completed": true}' | jq .
{
  "ID": 9,
  "CreatedAt": "2019-03-18T23:37:00.473936245+07:00",
  "UpdatedAt": "2019-03-18T23:37:00.473936245+07:00",
  "DeletedAt": null,
  "Title": "ABC",
  "Completed": true
}

Fetch all notes

$ curl http://localhost:8080/note | jq .
[
  {
    "ID": 1,
    "CreatedAt": "2019-03-18T23:12:53+07:00",
    "UpdatedAt": "2019-03-18T23:12:53+07:00",
    "DeletedAt": null,
    "Title": "",
    "Completed": false
  },
  {
    "ID": 2,
    "CreatedAt": "2019-03-18T23:13:18+07:00",
    "UpdatedAt": "2019-03-18T23:13:18+07:00",
    "DeletedAt": null,
    "Title": "ABC",
    "Completed": true
  },
  {
    "ID": 3,
    "CreatedAt": "2019-03-18T23:31:16+07:00",
    "UpdatedAt": "2019-03-18T23:31:16+07:00",
    "DeletedAt": null,
    "Title": "ABC",
    "Completed": true
  },
  {
    "ID": 4,
    "CreatedAt": "2019-03-18T23:31:26+07:00",
    "UpdatedAt": "2019-03-18T23:31:26+07:00",
    "DeletedAt": null,
    "Title": "ABC",
    "Completed": true
  },
  {
    "ID": 5,
    "CreatedAt": "2019-03-18T23:31:49+07:00",
    "UpdatedAt": "2019-03-18T23:31:49+07:00",
    "DeletedAt": null,
    "Title": "ABC",
    "Completed": true
  }
]

Need full source code?

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", "default:secret@/notes?charset=utf8&parseTime=True&loc=Local")
  if err != nil {
    panic("failed to connect database")
  }

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

func main() {

  router := gin.Default()

  v1 := router.Group("/api/v1/note")
  {
    v1.POST("/", createNote)
    v1.GET("/", fetchAllNote)
    v1.GET("/:id", fetchSingleNote)
    v1.PUT("/:id", updateNote)
    v1.DELETE("/:id", deleteNote)
  }
  router.Run()

}

type (
  // noteModel describes a noteModel type
  noteModel struct {
    gorm.Model
    Title     string `json:"title"`
    Completed int    `json:"completed"`
  }

  // transformedNote represents a formatted note
  transformedNote struct {
    ID        uint   `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
  }
)

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

// fetchAllNote fetch all notes
func fetchAllNote(c *gin.Context) {
  var notes []noteModel
  var _notes []transformedNote

  db.Find(&notes)

  if len(notes) <= 0 {
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No note found!"})
    return
  }

  //transforms the notes for building a good response
  for _, item := range notes {
    completed := false
    if item.Completed == 1 {
      completed = true
    } else {
      completed = false
    }
    _notes = append(_notes, transformedNote{ID: item.ID, Title: item.Title, Completed: completed})
  }
  c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _notes})
}

// fetchSingleNote fetch a single note
func fetchSingleNote(c *gin.Context) {
  var note noteModel
  noteID := c.Param("id")

  db.First(&note, noteID)

  if note.ID == 0 {
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No note found!"})
    return
  }

  completed := false
  if note.Completed == 1 {
    completed = true
  } else {
    completed = false
  }

  _note := transformedNote{ID: note.ID, Title: note.Title, Completed: completed}
  c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _note})
}

// updateNote update a note
func updateNote(c *gin.Context) {
  var note noteModel
  noteID := c.Param("id")

  db.First(&note, noteID)

  if note.ID == 0 {
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No note found!"})
    return
  }

  db.Model(&note).Update("title", c.PostForm("title"))
  completed, _ := strconv.Atoi(c.PostForm("completed"))
  db.Model(&note).Update("completed", completed)
  c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Note updated successfully!"})
}

// deleteNote remove a note
func deleteNote(c *gin.Context) {
  var note noteModel
  noteID := c.Param("id")

  db.First(&note, noteID)

  if note.ID == 0 {
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No note found!"})
    return
  }

  db.Delete(&note)
  c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Note deleted successfully!"})
}

Note: When you are using code production you must take care of the steps below:

  1. Do not fetch all the data select * from notes , use pagination
  2. Do not trust user input. You must validate the inputs, there are severals tools to validate input.
  3. Check every possible error
  4. You should use logging and authentication as your need

Credits