Nafeem Haque

Software Engineer

17 Feb 2023

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.