Today, I’m going to build a simple API for a to-do application using the Go programming language. For this, I’ll use Gin, one of Go’s simplest and fastest web frameworks, and Gorm, a powerful and flexible ORM for database operations.
To get started, you’ll need to install these packages. Navigate to your workspace directory ($GOPATH/src) and run the following commands:
$ 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:
- POST todos/
- GET todos/
- GET todos/{id}
- PUT todos/{id}
- DELETE todos/{id}
Let’s start coding! First, navigate to your $GOPATH/src
directory and create a folder named todo. Inside the todo directory, create a new file called main.go.
Now, import the Gin framework into your project and set up the routes. I prefer to add a version prefix like api/v1/
to the API endpoints, so we’ll use Gin’s Group method to organize the routes. Here’s how your main.go file should look:
package main
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()
}
Now that we’ve created five routes to handle functions like createTodo, fetchAllTodo, etc., let’s set up the database connection. We’ll use Gorm as our ORM and the MySQL dialect for database operations.
package main
import (
"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{})
}
In the code above, the “mysql” is our database driver, “root” is the database username
, “12345” is the password
, “demo” is the database name
. Feel free to replace these values with your specific database configuration.
We’ll use a Database function to manage the database connection, making it reusable across the application. Additionally, let’s create two structs:
todoModel
: Represents the original Todo data as stored in the database.transformedTodo
: Represents the API response format, excluding fields likecreated_at
andupdated_at
, which should not be exposed to consumers.
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"`
}
)
The Todo struct has an additional field called gorm.Model
. This field embeds the Model struct, which contains four fields: ID
, CreatedAt
, UpdatedAt
, and DeletedAt
.
Gorm provides migration facilities, which we have already used in the init function. When we run the application, it will first establish the database connection, and then perform the migration. This ensures that the necessary database tables are created or updated based on the struct definitions.
//Migrate the schema
db.AutoMigrate(&todoModel{})
Do you remember the five routes we defined earlier? Let’s implement the five methods one by one.
When a user sends a POST request to the path api/v1/todos/
with the title and completed fields, it will be handled by the createTodo function in the route v1.POST("/" , createTodo)
.
Let’s now implement the createTodo function.
// 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})
}
In the code above, we use the Gin Context to receive the posted data and the Gorm database connection to save the todo. After saving the resource, we send the resource ID back to the user along with a meaningful and informative response.
Now, let’s implement the remaining functions.
// 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!"})
}
In the fetchAllTodos
function, we fetch all the todos and build a transformed response with only the id, title, and completed fields. We exclude the CreatedAt, UpdatedAt, and DeletedAt fields, and we cast the integer value of Completed to a boolean.
Now that we’ve written enough code, let’s build the app and test it. I will test it using the Postman Chrome extension (you can use any REST client like curl to test).
To build the app, open your terminal and navigate to 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 todo app is running on port: 8080. It’ll display the debug log, because by default gin run’s in debug mode and port 8080.