Gin-Gonic : Rest API implementation for a `todo` resource
Initial Steps
- First create a directory
- Go to that directory
- Initialize with ‘go mod init todoapp’, usually it is a good practice to initialize with something like
github.com/{username}/{reponame}
- Run
go get -u github.com/gin-gonic/gin
from cli
now let’s create an empty main.go
file, and have the basic code like this:
package main
import (
"fmt"
)
func main() {
fmt.Println("hello,world!")
}
if you run this program by go run main.go
you will get the following output:
hello,world!
Now let’s start writing our application. Before getting into writing all the endpoints, lets create a simple indexhandler which will just return a simple message.
First, we will need to import the gin-gonic library by adding "github.com/gin-gonic/gin"
inside the import block:
import (
"fmt"
"github.com/gin-gonic/gin"
)
At this point we have the proper imports, lets now create a router inside our main function:
func main() {
fmt.Println("hello,world!")
router := gin.New()
}
notice line 3
, we will get a pointer of gin.Engine, by calling gin.New()
and we are going to store it in a variable router
for further use.
we can now use the router to create our first index handler. like this :
func main() {
fmt.Println("hello,world!")
router := gin.New()
router.GET("/", func(ctx *gin.Context) {
ctx.String(200, "msg:%s", "index handler called")
})
}
now we have a index handler, lets run the program in 3000
port. our code so far :
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
fmt.Println("hello,world!")
router := gin.New()
router.GET("/", func(ctx *gin.Context) {
ctx.String(200, "msg:%s", "index handler called")
})
router.Run(":3000")
}
when you run the following code, this should provide some warnings, lets ignore those for now. at the end of the console you should get something similiar : [GIN-debug] Listening and serving HTTP on :3000
, if you get this message, you have successfully created a running server.
now lets test our program, you can use the browser as well, I will use curl. by running curl localhost:3000
you will get the response as follows : msg:index handler called
, this is a string response, with 200 code, which means the request is ok or successfully completed.
okay so we have our basic server running, now lets get into the real todo resource creating. we will define a structure for todo like this:
type Todo struct {
Id int `json:"id"`
Title string `json:"title"`
IsCompleted bool `json:"is_completed"`
}
notice we have added json
tagging to have full control over the json request and response structure and naming.
for this tutorial we are not going to implement an actual database. but we will keep a slice structure to demonstrate.
with our in memory mock datas our program so far is:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type Todo struct {
Id int `json:"id"`
Title string `json:"title"`
IsCompleted bool `json:"is_completed"`
}
var Collection = []Todo{
{
Id: 1,
Title: "Meet Joey",
IsCompleted: false,
},
{
Id: 2,
Title: "Send Phoebe Flowers",
IsCompleted: false,
},
}
func main() {
fmt.Println("hello,world!")
router := gin.New()
router.GET("/", func(ctx *gin.Context) {
ctx.String(200, "msg:%s", "index handler called")
})
router.Run(":3000")
}
lets create a endpoint to list all the datas, like following:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type Todo struct {
Id int `json:"id"`
Title string `json:"title"`
IsCompleted bool `json:"is_completed"`
}
var Collection = []Todo{
{
Id: 1,
Title: "Meet Joey",
IsCompleted: false,
},
{
Id: 2,
Title: "Send Phoebe Flowers",
IsCompleted: false,
},
}
func main() {
fmt.Println("hello,world!")
router := gin.New()
router.GET("/", func(ctx *gin.Context) {
ctx.String(200, "msg:%s", "index handler called")
})
router.GET("/todo",func(ctx *gin.Context) {
ctx.JSON(200,Collection)
})
router.Run(":3000")
}
if we re-run our server and curl at localhost:3000/todo
we should get the following output:
[{"id":1,"title":"Meet Joey","is_completed":false},{"id":2,"title":"Send Phoebe Flowers","is_completed":false}]
this is a json response with 200 code.
now let’s create a endpoint to post data as following:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type Todo struct {
Id int `json:"id"`
Title string `json:"title"`
IsCompleted bool `json:"is_completed"`
}
var Collection = []Todo{
{
Id: 1,
Title: "Meet Joey",
IsCompleted: false,
},
{
Id: 2,
Title: "Send Phoebe Flowers",
IsCompleted: false,
},
}
func main() {
fmt.Println("hello,world!")
router := gin.New()
router.GET("/", func(ctx *gin.Context) {
ctx.String(200, "msg:%s", "index handler called")
})
router.GET("/todo", func(ctx *gin.Context) {
ctx.JSON(200, Collection)
})
router.POST("/todo", func(ctx *gin.Context) {
var req Todo
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(500, "binding error")
return
}
Collection = append(Collection, req)
ctx.JSON(200, "successfully created")
})
router.Run(":3000")
}
lets run and put curl request as follows :
curl --location 'localhost:3000/todo' \
--header 'Content-Type: application/json' \
--data '{
"id":3,
"title":"Call Rachel",
"is_completed":false
}'
this should provide the response "successfully created"
, DYI (do it yourself) : try providing wrong data and see what happens and
call the “/todo” with “GET” method to see if your added todo is on the list or not.
we will skip explaining the single get todo endpoint, this is rather easy and you will understand just looking at the code:
package main
import (
"fmt"
"strconv"
"github.com/gin-gonic/gin"
)
type Todo struct {
Id int `json:"id"`
Title string `json:"title"`
IsCompleted bool `json:"is_completed"`
}
var Collection = []Todo{
{
Id: 1,
Title: "Meet Joey",
IsCompleted: false,
},
{
Id: 2,
Title: "Send Phoebe Flowers",
IsCompleted: false,
},
}
func main() {
fmt.Println("hello,world!")
router := gin.New()
router.GET("/", func(ctx *gin.Context) {
ctx.String(200, "msg:%s", "index handler called")
})
router.GET("/todo", func(ctx *gin.Context) {
ctx.JSON(200, Collection)
})
router.POST("/todo", func(ctx *gin.Context) {
var req Todo
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(500, "binding error")
return
}
Collection = append(Collection, req)
ctx.JSON(200, "successfully created")
})
router.GET("/todo/:id", func(ctx *gin.Context) {
idStr, has := ctx.Params.Get("id")
if !has {
ctx.JSON(500, "missing id")
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
ctx.JSON(500, "not valid id")
return
}
for _, todo := range Collection {
if todo.Id == id {
ctx.JSON(200, todo)
return
}
}
ctx.JSON(500, "todo not found")
})
router.Run(":3000")
}
lets now create the update and delete endpoints as the following.
package main
import (
"fmt"
"strconv"
"github.com/gin-gonic/gin"
)
type Todo struct {
Id int `json:"id"`
Title string `json:"title"`
IsCompleted bool `json:"is_completed"`
}
var Collection = []Todo{
{
Id: 1,
Title: "Meet Joey",
IsCompleted: false,
},
{
Id: 2,
Title: "Send Phoebe Flowers",
IsCompleted: false,
},
}
func main() {
fmt.Println("hello,world!")
router := gin.New()
router.GET("/", func(ctx *gin.Context) {
ctx.String(200, "msg:%s", "index handler called")
})
router.GET("/todo", func(ctx *gin.Context) {
ctx.JSON(200, Collection)
})
router.POST("/todo", func(ctx *gin.Context) {
var req Todo
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(500, "binding error")
return
}
Collection = append(Collection, req)
ctx.JSON(200, "successfully created")
})
router.GET("/todo/:id", func(ctx *gin.Context) {
idStr, has := ctx.Params.Get("id")
if !has {
ctx.JSON(500, "missing id")
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
ctx.JSON(500, "not valid id")
return
}
for _, todo := range Collection {
if todo.Id == id {
ctx.JSON(200, todo)
return
}
}
ctx.JSON(500, "todo not found")
})
router.PUT("/todo/:id", func(ctx *gin.Context) {
idStr, has := ctx.Params.Get("id")
if !has {
ctx.JSON(500, "missing id")
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
ctx.JSON(500, "not valid id")
return
}
var req Todo
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(500, "binding error")
return
}
for i, todo := range Collection {
if todo.Id == id {
todo.Title = req.Title
todo.IsCompleted = req.IsCompleted
Collection = append(Collection[:i], todo)
ctx.JSON(200, todo)
return
}
}
ctx.JSON(500, "resource not found to update")
})
router.DELETE("/todo/:id", func(ctx *gin.Context) {
idStr, has := ctx.Params.Get("id")
if !has {
ctx.JSON(500, "missing id")
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
ctx.JSON(500, "not valid id")
return
}
for i, todo := range Collection {
if todo.Id == id {
Collection = append(Collection[:i], Collection[i+1:]...)
ctx.JSON(200, "successful")
return
}
}
ctx.JSON(500, "todo not found")
})
router.Run(":3000")
}
DYI: now run the program and test the added endpoints.
With this we have completed our rest application. And we have successfully created the following endpoints.
GET: / (index)
GET: /todo (get all todos)
POST: /todo (create a new todo)
GET: /todo/:id (get single todo)
PUT: /todo/:id (update todo)
DELETE: /todo/:id (remove a todo)
notes:
- This is a starter code
- There are lot of aspects we could have improved
- We will learn the improvements in future posts
Thanks a lot for coding and learning with me.