This page looks best with JavaScript enabled

Learning Golang with Fiber

 ·   ·  ☕ 10 min read

I started with Golang not too long ago, and I loved the fact that I can create a web application with a couple of lines of code. But, as always frameworks help to take that web application to places. Being a practical person who develops apps for side projects and for a living, I cannot simply overstate this fact. A production application is not simply a matter of responding to a hello world JSON and frameworks take care of the routine tasks of providing structure, connecting to database, enforcing security and so on.

From get go, I liked Fiber framework.

golang-fiber_v2_logo

Fiber is an ExpressJS inspired framework -

  • easy for NodeJS developers to pick up
  • fast (I mean really fast - see benchmarks)
  • easy to develop
  • flexible

Coupled with Go’s simplicity, light-weight nature, ease of async programming and deployment, Fiber is a formidable (albeit simple) tool to quickly develop web apps.

Also see: a small-scale developer’s view of NodeJS vs Golang.

In this post, let us see an example of developing simple CRUD APIs in Fiber v2. We will not be using an ORM or a database to keep things focused on Fiber and Golang.

Start with Go

First, download and install Go on your computer.

Ensure that your favourite editor (= VSCode) has support to code Go programs.

If you are looking at this in 2020, you may also need to enable Gopls in VSCode to enable better module support and more modern features. Hit Ctrl + Shift + P > type Settings > Hit Enter. Type in @ext:golang.go gopls to see Go extension settings. Scroll down and enable Go: Use Language Server.

You are all set.

Project Structure

Create a folder on your computer and open that folder in VSCode.

Create a new file server.go. Code in the following -

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("hello world")
}

Open terminal in VSCode (Ctrl + ~) and run program.

1
go run server.go

You should see an output message if everything works fine.

Get started on Fiber

Enter below command in the VSCode terminal to install fiber.

1
go get github.com/gofiber/fiber/v2

We will now start coding the API in server.go.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
)


type Todo struct {
	ID        int    `json:"id"`
	Name      string `json:"name"`
	Completed bool   `json:"completed"`
}

var todos = []Todo{
	{ID: 1, Name: "abc", Completed: false},
	{ID: 2, Name: "def", Completed: true},
}

func main() {

	app := fiber.New()
	app.Use(logger.New())

	app.Get("/", func(c *fiber.Ctx) error {
		return c.SendString("Hello, World!")
    })

    app.Listen(":5000")
}

We have done a few simple things here -

  • defined a basic structure for our todo using type Todo struct {}. We also provided the JSON equivalents of the fields
  • created a simple array of type Todo - var todos = []Todo{{ID: 1, Name: "abc", Completed: false},}
  • initiated fiber with app := fiber.New()
  • enabled logging input requests with a single line app.Use(logger.New())
  • enabled a simple response at root using app.Get("/", ...)

Run the program again using go run server.go.

Use a REST client (like Insomnia), invoke your API at http://localhost:5000/ to see a hello world response.

You can start seeing similarities with Express server in the simple program - more will follow.

At this stage, you may observe that any change you make will require a restart of server. There are multiple ways of solving this problem including using our dear own nodemon. Let us stick to a Go solution though - we will use a package called air to monitor for changes in our project folder and restart server whenever there are changes.

In your VSCode terminal, input -

1
https://github.com/cosmtrek/air

You can stop your own Go server and simply type -

1
air

The above command will run your server and automatically restart it whenever there are changes.

Onwards then.

(Pseudo) CRUD APIs in Fiber

Let us code in the APIs in our program.

Get Request

Introduce below line in main function before app.Listen(":5000") line.

1
app.Get("/todo", getTodo)

Create a new function below main.

1
2
3
func getTodo(c *fiber.Ctx) error {
	return c.Status(fiber.StatusOK).JSON(todos)
}

The function is (more or less) self-explanatory. The pointer c is fiber context that is injected in our function by fiber framework. We just return an ok with our todos array data. Simple enough.

Test the get API using the URL http://localhost:5000/todo and GET method from the REST client. You will see the below JSON response.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[
  {
    "id": 1,
    "name": "abc",
    "completed": false
  },
  {
    "id": 2,
    "name": "def1",
    "completed": false
  }
]

Post Request

Introduce a new line after GET request in server.go.

1
	app.Post("/todo", postTodo)

As it was earlier, create a new function called postTodo -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func postTodo(c *fiber.Ctx) error {
	type request struct {
		Name      string `json:"name"`
		Completed bool   `json:"completed"`
	}

	var body request

	err := c.BodyParser(&body)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Cannot parse JSON",
		})
	}

	todo := Todo{
		ID:        len(todos) + 1,
		Name:      body.Name,
		Completed: body.Completed,
	}

	todos = append(todos, todo)

	return c.Status(fiber.StatusCreated).JSON(todo)
}

We have accomplished quite a bit, but nothing out of ordinary -

  • specified how our input JSON will look like (request struct)
  • included a body parser middleware to fetch input JSON data
  • fetched the incoming data using todo variable
  • appended todo to our todos array
  • returned a success message back

The below code block takes care of error checks (in our case, this handles parse errors) -

1
2
3
4
5
6
    err := c.BodyParser(&body)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Cannot parse JSON",
		})
	}

This type of error handling is typical to a Go program. Error checks are handled by an if statement that checks error returned from a previous statement.

Create a POST request in your REST client - http://localhost:5000/todo. Input the following JSON as input -

1
2
3
4
{
  "name": "xyz",
  "completed": true
}

Send the request and you will get the same response back. Do a GET request with the same URL and you will see the additional todo record in the response.

Of course, we are dealing only with a variable that stays in memory. The changes will disappear when the server shutsdown or restarts.

Note: the id generation logic is flaky. We used the length of the array and increment by 1. Ergo, deleting a record can lead to duplicate ids.

Delete Request

You know the drill by now.

Introduce a new REST service in main..

1
	app.Delete("/todo/:id", deleteTodo)

Create a new function..

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func deleteTodo(c *fiber.Ctx) error {
	paramID := c.Params("id")
	id, err := strconv.Atoi(paramID)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Invalid id.",
		})
	}

	for i, todo := range todos {
		if todo.ID == id {
			todos = append(todos[0:i], todos[i+1:]...)
			return c.Status(fiber.StatusOK).JSON(todo)
		}
	}

	return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Record not found"})
}

We have used a new function for string type conversion. Include the package in the import statement.

1
2
3
4
5
6
import (
	"strconv"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
)

A couple of things to take note -

  1. We are not manipulating the array directly since the array itself but creating a new array each time there’s a change with this line todos = append(todos[0:i], todos[i+1:]...). Not effective, but works. Instead we could have used a pointer and updated array directly.
  2. We are using a convenient shortcut provided by fiber to create response JSON on the go - fiber.Map{"error": "Record not found"}

Patch Request

Patch is similar to delete - except that we change an element instead of removing it.

Include a new function call in main.

1
2
	app.Patch("/todo/:id", patchTodo)

Create the function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func patchTodo(c *fiber.Ctx) error {
	type request struct {
		Name      string `json:"name"`
		Completed bool   `json:"completed"`
	}
	var body request

	err := c.BodyParser(&body)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Cannot parse JSON",
		})
	}

	paramID := c.Params("id")
	id, err := strconv.Atoi(paramID)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Invalid id.",
		})
	}

	for i, todo := range todos {
		if todo.ID == id {
			todos[i] = Todo{
				ID:        id,
				Name:      body.Name,
				Completed: body.Completed,
			}
			return c.Status(fiber.StatusOK).JSON(todos[i])
		}
	}

	return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Record not found"})
}

All Code in One Place

We have created a program using Fiber to perform CRUD operations, but on a local variable. With a couple of more packages and a few lines of code we could connect to a DB and get to a more real-world program.

The below code represents the final state. You may also refer to this Git repo for the full code.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package main

import (
	"strconv"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
)

// Todo struct!
type Todo struct {
	ID        int    `json:"id"`
	Name      string `json:"name"`
	Completed bool   `json:"completed"`
}

var todos = []Todo{
	{ID: 1, Name: "abc", Completed: false},
	{ID: 2, Name: "def", Completed: true},
}

func main() {

	app := fiber.New()
	app.Use(logger.New())

	app.Get("/", func(c *fiber.Ctx) error {
		return c.SendString("Hello, World!")
	})

	app.Get("/todo", getTodo)
	app.Post("/todo", postTodo)
	app.Get("/todo/:id", getSingleTodo)
	app.Delete("/todo/:id", deleteTodo)
	app.Patch("/todo/:id", patchTodo)

	app.Listen(":5000")
}

func getTodo(c *fiber.Ctx) error {
	return c.Status(fiber.StatusOK).JSON(todos)
}

func getSingleTodo(c *fiber.Ctx) error {
	paramID := c.Params("id")
	id, err := strconv.Atoi(paramID)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "ID invalid.",
		})
	}

	for _, todo := range todos {
		if todo.ID == id {
			return c.Status(fiber.StatusFound).JSON(todo)
		}
	}

	return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Record not found"})
}

func deleteTodo(c *fiber.Ctx) error {
	paramID := c.Params("id")
	id, err := strconv.Atoi(paramID)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Invalid id.",
		})
	}

	for i, todo := range todos {
		if todo.ID == id {
			todos = append(todos[0:i], todos[i+1:]...)
			return c.Status(fiber.StatusOK).JSON(todo)
		}
	}

	return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Record not found"})
}

func postTodo(c *fiber.Ctx) error {
	type request struct {
		Name      string `json:"name"`
		Completed bool   `json:"completed"`
	}

	var body request

	err := c.BodyParser(&body)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Cannot parse JSON",
		})
	}

	todo := Todo{
		ID:        len(todos) + 1,
		Name:      body.Name,
		Completed: body.Completed,
	}

	todos = append(todos, todo)

	return c.Status(fiber.StatusCreated).JSON(todo)
}

func patchTodo(c *fiber.Ctx) error {
	type request struct {
		Name      string `json:"name"`
		Completed bool   `json:"completed"`
	}
	var body request

	err := c.BodyParser(&body)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Cannot parse JSON",
		})
	}

	paramID := c.Params("id")
	id, err := strconv.Atoi(paramID)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Invalid id.",
		})
	}

	for i, todo := range todos {
		if todo.ID == id {
			todos[i] = Todo{
				ID:        id,
				Name:      body.Name,
				Completed: body.Completed,
			}
			return c.Status(fiber.StatusOK).JSON(todos[i])
		}
	}

	return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Record not found"})
}

Now go create your magic. Enjoy Go!

Stay in touch!
Share on

Prashanth Krishnamurthy
WRITTEN BY
Prashanth Krishnamurthy
Technologist | Creator of Things