Golang for Web (Part-I): Build your first REST API with Golang & Fiber
Tags: go, gofiber, webdev, beginners
Published: 2020-11-25 18:42:56

This is the first part of this series on **Golang for Web**. My goal is to show the newcomers how easily we can build REST APIs with Go by breaking down big apps into small ones. Here, we're going to build a simple TODO app webserver with Gofiber by separating all routes, controllers, etc.

## Go: An open-source programming language

Go is an open-source programming language that makes it easy to build **simple**, **reliable**, and **efficient** software.

Go is a statically typed, compiled programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. Go is syntactically similar to C, but with memory safety, garbage collection, structural typing, and CSP-style concurrency. (Source: Wikipedia)

![Interesting](https://dev-to-uploads.s3.amazonaws.com/i/w14qbq043f6y06yeabqp.gif)

## Golang for Web 🌐

As a MERN stack developer, I found [Fiber](https://docs.gofiber.io) Web Framework is very similar to express as they are claiming and this is pretty easy for a JS developer to get started with Golang to build a nice and amazing REST API.

**Fiber** is an Express inspired **web framework** built on top of Fasthttp, the **fastest** HTTP engine for Go. Designed to **ease** things up for **fast** development with **zero memory allocation** and **performance** in mind.

Check their amazing benchmark performance here - https://docs.gofiber.io/extra/benchmarks

## What will we build? 🤔

Okay enough talk 🤚 It's time to show some action with Fiber. Today we are going to build a basic TODO app with Fiber. I'm pretty sure after this, you can easily start building REST APIs with Golang 👨🏻‍💻

## Prerequisites 📝

-   A basic knowledge of Golang syntax.
-   Go version 1.14 or above installed on your machine. [Install from here](https://golang.org/)
-   Postman or any other related app installed on your machine. [Download from here](https://www.postman.com/downloads/)

If you have these, Let's get started 🚀

![Yes Please](https://dev-to-uploads.s3.amazonaws.com/i/1b2bnds5byu6e8p07r5k.gif)

## Let’s Begin 🏁

### 1. Setup our Project

First, let’s create a new directory named `fiber-todo` and open this directory in VS Code (or any other Code Editor/ IDE)

Now at the root directory open your terminal and run the command :

```shell
go mod init github.com/<Your_Username>/<Repo_Name>
```

Example :

```shell
go mod init github.com/devsmranjan/golang-fiber-basic-todo-app
```

Let's install fiber in our project by running :

```shell
go get -u github.com/gofiber/fiber/v2
```

Now inside our root directory create a file named `main.go` and create two directories `routes` & `controllers`.

### 2. Create our first server

Now inside `main.go` write the following code :

```go
package main

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

func main() {}
```

Okay, you will get some errors here but stay tuned with me 🤝

Now inside the `main()` method let's initiate fiber.

```go
app := fiber.New()
```

Add our first route by :

```go
app.Get("/", func(c *fiber.Ctx) error {
	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"success":  true,
		"message": "You are at the endpoint 😉",
	})
})
```

and finally, listen to the server at 8000 port and catch the error if any.

```go
err := app.Listen(":8000")

if err != nil {
	panic(err)
}
```

Here is our final code :

```go
package main

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

func main() {
	app := fiber.New()

	// give response when at /
	app.Get("/", func(c *fiber.Ctx) error {
		return c.Status(fiber.StatusOK).JSON(fiber.Map{
			"success":  true,
			"message": "You are at the endpoint 😉",
		})
	})

	// Listen on server 8000 and catch error if any
	err := app.Listen(":8000")

	// handle error
	if err != nil {
		panic(err)
	}
}
```

Now to run our server, open terminal and in our root directory run :

```shell
go run main.go
```

It will show something like this

```shell

 ┌───────────────────────────────────────────────────┐
 │                    Fiber v2.2.0                   │
 │               http://127.0.0.1:8000               │
 │                                                   │
 │ Handlers ............. 2  Threads ............. 4 │
 │ Prefork ....... Disabled  PID ............. 60983 │
 └───────────────────────────────────────────────────┘

```

Open your browser now, and goto `localhost:8000`. You will get the output like this

```json
{
    "message": "You are at the endpoint 😉",
    "success": true
}
```

Hureeey!!! We did it 🤟

![You did it](https://dev-to-uploads.s3.amazonaws.com/i/sid9xbr8nsswi5yblecp.gif)

Additionally, if you want to add **logger** middleware, then run

```shell
go get -u github.com/gofiber/fiber/v2/middleware/logger
```

Import logger in our project

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

and finally, connect this middleware with our `app`

```go
app.Use(logger.New())
```

Here is our final code :

```go
package main

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

func main() {
	app := fiber.New()
	app.Use(logger.New()) // new

	// give response when at /
	app.Get("/", func(c *fiber.Ctx) error {
		return c.Status(fiber.StatusOK).JSON(fiber.Map{
			"success":  true,
			"message": "You are at the endpoint 😉",
		})
	})

	// Listen on server 8000 and catch error if any
	err := app.Listen(":8000")

	// handle error
	if err != nil {
		panic(err)
	}
}
```

Now after running our server again, goto `localhost:8000` and you can see the terminal is showing the log of our request like below :

```shell
21:44:48 | 200 |      0s |       127.0.0.1 | GET     | /
```

### 3. Decide Endpoints for our TODO app 🔗

`localhost:8000/api/todos`

```text
request: GET
description: To get all todos
```

`localhost:8000/api/todos/:id`

```text
request: GET
description: Get todo by id
```

`localhost:8000/api/todos`

```text
request: POST
input: {
    title : String
}
description: Create new todo
```

`localhost:8000/api/todos/:id`

```text
request: PUT
input: {
    title : String,
    completed : Boolean
}
description: Update todo
```

`localhost:8000/api/todos/:id`

```text
request: DELETE
description: Delete todo
```

### 4. Build our first API endpoint 🏗

#### Step 1 :

Let's write our first controller for our todo app.

Open `controllers` directory and create a file named `todo.go`

Now inside `controllers/todo.go` let's do our required imports.

```go
package controllers

import (
	"github.com/gofiber/fiber/v2"
)
```

Add `Todo` structure

```go
type Todo struct {
	Id        int    `json:"id"`
	Title     string `json:"title"`
	Completed bool   `json:"completed"`
}
```

Let's add some predefined todos

```go
var todos = []*Todo{
	{
		Id:        1,
		Title:     "Walk the dog 🦮",
		Completed: false,
	},
	{
		Id:        2,
		Title:     "Walk the cat 🐈",
		Completed: false,
	},
}
```

and now finally let's create our controller to **get all the todos**

```go
func GetTodos(c *fiber.Ctx) error {
	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"success": true,
		"data": fiber.Map{
			"todos": todos,
		},
	})
}
```

Here is our final code in `controllers/todo.go` :

```go
package controllers

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

type Todo struct {
	Id        int    `json:"id"`
	Title     string `json:"title"`
	Completed bool   `json:"completed"`
}

var todos = []*Todo{
	{
		Id:        1,
		Title:     "Walk the dog 🦮",
		Completed: false,
	},
	{
		Id:        2,
		Title:     "Walk the cat 🐈",
		Completed: false,
	},
}

// get all todos
func GetTodos(c *fiber.Ctx) error {
	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"success": true,
		"data": fiber.Map{
			"todos": todos,
		},
	})
}
```

#### Step 2 :

Now let's write our first route for our todo app and connect our controllers.

Open the `routes` directory and create a file named `todo.go`

Now inside `routes/todo.go` let's do our required imports

```go
package routes

import (
	"github.com/gofiber/fiber/v2"
	"github.com/devsmranjan/golang-fiber-basic-todo-app/controllers" // replace
)
```

Replace `github.com/devsmranjan/golang-fiber-basic-todo-app/controllers` with your github repo url like `github.com/<Your_Username>/<Repo_Name>/controllers`

Create our first route to get all todos

```go
func TodoRoute(route fiber.Router) {
	route.Get("", controllers.GetTodos)
}
```

Here is our final code in `routes/todo.go` :

```go
package routes

import (
	"github.com/gofiber/fiber/v2"
	"github.com/devsmranjan/golang-fiber-basic-todo-app/controllers" // replace
)

func TodoRoute(route fiber.Router) {
	route.Get("", controllers.GetTodos)
}
```

#### Step 3 :

Okay here is the final step.
Go back to `main.go` and import `routes`

```go
import (
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/devsmranjan/golang-fiber-basic-todo-app/routes" // new // replace
)
```

Replace `github.com/devsmranjan/golang-fiber-basic-todo-app/routes` with your github repo url i.e `github.com/<Your_Username>/<Repo_Name>/routes`

Now let's create a separate function to handle all our routes.

```go
func setupRoutes(app *fiber.App) {}
```

Move all routes from `main()` method to `setupRoutes()` method.

```go
func setupRoutes(app *fiber.App) {

    // moved from main method
	app.Get("/", func(c *fiber.Ctx) error {
		return c.Status(fiber.StatusOK).JSON(fiber.Map{
			"success":  true,
			"message": "You are at the endpoint 😉",
		})
	})
}
```

Now call `setupRoutes()` method from `main()` method.

```go
setupRoutes(app)
```

Here is our final `main()` looks like.

```go
func main() {
	app := fiber.New()
	app.Use(logger.New())

	// setup routes
	setupRoutes(app) // new

	// Listen on server 8000 and catch error if any
	err := app.Listen(":8000")

	// handle error
	if err != nil {
		panic(err)
	}
}
```

Now let's make a routes group named `api` inside our `setupRoutes()` method

```go
api := app.Group("/api")
```

Add response for `/api` route

```go
api.Get("", func(c *fiber.Ctx) error {
	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"success": true,
		"message": "You are at the api endpoint 😉",
	})
})
```

Now finally, connect all the todo routes to our `api` route group

```go
routes.TodoRoute(api.Group("/todos"))
```

Final `setupRoutes()` method looks like :

```go
func setupRoutes(app *fiber.App) {
	// give response when at /
	app.Get("/", func(c *fiber.Ctx) error {
		return c.Status(fiber.StatusOK).JSON(fiber.Map{
			"success":  true,
			"message": "You are at the endpoint 😉",
		})
	})

	// api group
	api := app.Group("/api")

	// give response when at /api
	api.Get("", func(c *fiber.Ctx) error {
		return c.Status(fiber.StatusOK).JSON(fiber.Map{
			"success": true,
			"message": "You are at the api endpoint 😉",
		})
	})

	// connect todo routes
	routes.TodoRoute(api.Group("/todos"))
}
```

Now let's run our server and go to `http://localhost:8000/api/todos`. The output will be like below.

```json
{
    "data": {
        "todos": [
            {
                "id": 1,
                "title": "Walk the dog 🦮",
                "completed": false
            },
            {
                "id": 2,
                "title": "Walk the cat 🐈",
                "completed": false
            }
        ]
    },
    "success": true
}
```

![Yeah](https://dev-to-uploads.s3.amazonaws.com/i/blx7imzpdqokaxx6ccr5.gif)

### 5. Let's create other controllers 🎛

I guess you got the idea, how all are working. Right??? 😎

Now let's create other controllers.
But before that let's add some required imports in `controllers/todo.go` :

```go
import (
	"fmt" // new
	"strconv" // new

	"github.com/gofiber/fiber/v2"
)
```

Okay. Let's create a controller to **create a todo**

```go
func CreateTodo(c *fiber.Ctx) error {
	type Request struct {
		Title string `json:"title"`
	}

	var body Request

	err := c.BodyParser(&body)

	// if error
	if err != nil {
		fmt.Println(err)
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success":  false,
			"message": "Cannot parse JSON",
		})
	}

	// create a todo variable
	todo := &Todo{
		Id:        len(todos) + 1,
		Title:     body.Title,
		Completed: false,
	}

	// append in todos
	todos = append(todos, todo)

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{
		"success": true,
		"data": fiber.Map{
			"todo": todo,
		},
	})
}
```

A controller to **get a single todo by id**

```go
func GetTodo(c *fiber.Ctx) error {
	// get parameter value
	paramId := c.Params("id")

	// convert parameter value string to int
	id, err := strconv.Atoi(paramId)

	// if error in parsing string to int
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success":  false,
			"message": "Cannot parse Id",
		})
	}

	// find todo and return
	for _, todo := range todos {
		if todo.Id == id {
			return c.Status(fiber.StatusOK).JSON(fiber.Map{
				"success": true,
				"data": fiber.Map{
					"todo": todo,
				},
			})
		}
	}

	// if todo not available
	return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
		"success": false,
		"message": "Todo not found",
	})
}
```

A controller to **update a todo**

```go
func UpdateTodo(c *fiber.Ctx) error {
	// find parameter
	paramId := c.Params("id")

	// convert parameter string to int
	id, err := strconv.Atoi(paramId)

	// if parameter cannot parse
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success": false,
			"message": "Cannot parse id",
		})
	}

	// request structure
	type Request struct {
		Title     *string `json:"title"`
		Completed *bool   `json:"completed"`
	}

	var body Request
	err = c.BodyParser(&body)

	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success": false,
			"message": "Cannot parse JSON",
		})
	}

	var todo *Todo

	for _, t := range todos {
		if t.Id == id {
			todo = t
			break
		}
	}

	if todo.Id == 0 {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"success":  false,
			"message": "Not found",
		})
	}

	if body.Title != nil {
		todo.Title = *body.Title
	}

	if body.Completed != nil {
		todo.Completed = *body.Completed
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"success": true,
		"data": fiber.Map{
			"todo": todo,
		},
	})
}
```

A controller to **delete a todo**

```go
func DeleteTodo(c *fiber.Ctx) error {
	// get param
	paramId := c.Params("id")

	// convert param string to int
	id, err := strconv.Atoi(paramId)

	// if parameter cannot parse
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success": false,
			"message": "Cannot parse id",
		})
	}

	// find and delete todo
	for i, todo := range todos {
		if todo.Id == id {

			todos = append(todos[:i], todos[i+1:]...)

			return c.Status(fiber.StatusNoContent).JSON(fiber.Map{
				"success":  true,
				"message": "Deleted Succesfully",
			})
		}
	}

	// if todo not found
	return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
		"success": false,
		"message": "Todo not found",
	})
}
```

Here is our final code in `controllers/todo.go` :

```go
package controllers

import (
	"fmt"
	"strconv"

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

type Todo struct {
	Id        int    `json:"id"`
	Title     string `json:"title"`
	Completed bool   `json:"completed"`
}

var todos = []*Todo{
	{
		Id:        1,
		Title:     "Walk the dog 🦮",
		Completed: false,
	},
	{
		Id:        2,
		Title:     "Walk the cat 🐈",
		Completed: false,
	},
}

// get all todos
func GetTodos(c *fiber.Ctx) error {
	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"success": true,
		"data": fiber.Map{
			"todos": todos,
		},
	})
}

// Create a todo
func CreateTodo(c *fiber.Ctx) error {
	type Request struct {
		Title string `json:"title"`
	}

	var body Request

	err := c.BodyParser(&body)

	// if error
	if err != nil {
		fmt.Println(err)
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success":  false,
			"message": "Cannot parse JSON",
		})
	}

	// create a todo variable
	todo := &Todo{
		Id:        len(todos) + 1,
		Title:     body.Title,
		Completed: false,
	}

	// append in todos
	todos = append(todos, todo)

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{
		"success": true,
		"data": fiber.Map{
			"todo": todo,
		},
	})
}

// get a single todo
// PARAM: id
func GetTodo(c *fiber.Ctx) error {
	// get parameter value
	paramId := c.Params("id")

	// convert parameter value string to int
	id, err := strconv.Atoi(paramId)

	// if error in parsing string to int
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success":  false,
			"message": "Cannot parse Id",
		})
	}

	// find todo and return
	for _, todo := range todos {
		if todo.Id == id {
			return c.Status(fiber.StatusOK).JSON(fiber.Map{
				"success": true,
				"data": fiber.Map{
					"todo": todo,
				},
			})
		}
	}

	// if todo not available
	return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
		"success": false,
		"message": "Todo not found",
	})
}

// Update a todo
// PARAM: id
func UpdateTodo(c *fiber.Ctx) error {
	// find parameter
	paramId := c.Params("id")

	// convert parameter string to int
	id, err := strconv.Atoi(paramId)

	// if parameter cannot parse
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success": false,
			"message": "Cannot parse id",
		})
	}

	// request structure
	type Request struct {
		Title     *string `json:"title"`
		Completed *bool   `json:"completed"`
	}

	var body Request
	err = c.BodyParser(&body)

	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success": false,
			"message": "Cannot parse JSON",
		})
	}

	var todo *Todo

	for _, t := range todos {
		if t.Id == id {
			todo = t
			break
		}
	}

	if todo.Id == 0 {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"success":  false,
			"message": "Not found",
		})
	}

	if body.Title != nil {
		todo.Title = *body.Title
	}

	if body.Completed != nil {
		todo.Completed = *body.Completed
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"success": true,
		"data": fiber.Map{
			"todo": todo,
		},
	})
}

// Delete a todo
// PARAM: id
func DeleteTodo(c *fiber.Ctx) error {
	// get param
	paramId := c.Params("id")

	// convert param string to int
	id, err := strconv.Atoi(paramId)

	// if parameter cannot parse
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success": false,
			"message": "Cannot parse id",
		})
	}

	// find and delete todo
	for i, todo := range todos {
		if todo.Id == id {

			todos = append(todos[:i], todos[i+1:]...)

			return c.Status(fiber.StatusNoContent).JSON(fiber.Map{
				"success":  true,
				"message": "Deleted Succesfully",
			})
		}
	}

	// if todo not found
	return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
		"success": false,
		"message": "Todo not found",
	})
}
```

### 6. Create other routes 🚏

Let's create other routes for our todo app.

Now, inside `TodoRoute()` add all below routes

```go
route.Post("", controllers.CreateTodo)
route.Put("/:id", controllers.UpdateTodo)
route.Delete("/:id", controllers.DeleteTodo)
route.Get("/:id", controllers.GetTodo)
```

Finally, our final code looks like :

```go
package routes

import (
	"github.com/gofiber/fiber/v2"
	"github.com/devsmranjan/golang-fiber-basic-todo-app/controllers" // replace
)

func TodoRoute(route fiber.Router) {
	route.Get("", controllers.GetTodos)
	route.Post("", controllers.CreateTodo) // new
	route.Put("/:id", controllers.UpdateTodo) // new
	route.Delete("/:id", controllers.DeleteTodo) // new
	route.Get("/:id", controllers.GetTodo) // new
}
```

Now we are all set to run our server 😎
And let's test our API in Postman.

### 7. Test our endpoints 🧪

To get all todos, give a `GET` request to `localhost:8000/api/todos`

![get All todos](https://dev-to-uploads.s3.amazonaws.com/i/rogwt1s1cr9fw01ho9lg.png)

To get todo by id, give a `GET` request to `localhost:8000/api/todos/:id`
Here replace `:id` with a todo `id`

![Get todo by id](https://dev-to-uploads.s3.amazonaws.com/i/ak52n1m8cddg64nsnm9d.png)

To create a new todo, give a `POST` request to `localhost:8000/api/todos` with a `title: <String>` in the request body.

![Create new todo](https://dev-to-uploads.s3.amazonaws.com/i/2mmsyvru45m28xfzsm68.png)

Okay. Let's check if our todo is successfully created or not.
Give a `GET` request to `localhost:8000/api/todos` to get all todos again.

![Get all todos after creating new todo](https://dev-to-uploads.s3.amazonaws.com/i/r2l8p81nnd0w2lk2vg9x.png)

Yeeeeeeeeah !!! 🤩

Now, let's update a todo by giving a `PUT` request to `localhost:8000/api/todos/:id` with a `title: <String>` or `completed: <Boolean>` or both in the request body.
Here replace `:id` with a todo `id`

![Update todo](https://dev-to-uploads.s3.amazonaws.com/i/ic3ik72g8g8fobsyczp3.png)

To delete a todo, give a `DELETE` request to `localhost:8000/api/todos/:id`
Here replace `:id` with a todo `id`

![Delete todo](https://dev-to-uploads.s3.amazonaws.com/i/u271gsiv56lu8u1wujvn.png)

Congooooooo 🥳 🥳 🥳   We did it 💪🏻

![Did it](https://dev-to-uploads.s3.amazonaws.com/i/cyt4yf3jh3u7s3cygaji.gif)

## Conclusion 📋

For more information, I suggest taking a deeper look at the documentation here https://docs.gofiber.io/

Here is my GitHub link for this project - https://github.com/devsmranjan/golang-fiber-basic-todo-app

Thank you for reading my article 🙂 . I hope you have learned something here.

Happy coding 👨‍💻👩‍💻 and stay tuned for my next post in this series!

Thanks! Don't forget to give a ♥️ and follow :)