DEV Community

Cover image for Golang for Web (Part-I): Build your first REST API with Golang & Fiber
Smruti Ranjan Rana
Smruti Ranjan Rana

Posted on • Updated on

Golang for Web (Part-I): Build your first REST API with Golang & Fiber

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

Golang for Web 🌐

As a MERN stack developer, I found Fiber 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
  • Postman or any other related app installed on your machine. Download from here

If you have these, Let's get started 🚀

Yes Please

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 :

go mod init github.com/<Your_Username>/<Repo_Name>
Enter fullscreen mode Exit fullscreen mode

Example :

go mod init github.com/devsmranjan/golang-fiber-basic-todo-app
Enter fullscreen mode Exit fullscreen mode

Let's install fiber in our project by running :

go get -u github.com/gofiber/fiber/v2
Enter fullscreen mode Exit fullscreen mode

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 :

package main

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

func main() {}
Enter fullscreen mode Exit fullscreen mode

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

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

app := fiber.New()
Enter fullscreen mode Exit fullscreen mode

Add our first route by :

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

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

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

if err != nil {
    panic(err)
}
Enter fullscreen mode Exit fullscreen mode

Here is our final code :

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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

go run main.go
Enter fullscreen mode Exit fullscreen mode

It will show something like this


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

Enter fullscreen mode Exit fullscreen mode

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

{
    "message": "You are at the endpoint 😉",
    "success": true
}
Enter fullscreen mode Exit fullscreen mode

Hureeey!!! We did it 🤟

You did it

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

go get -u github.com/gofiber/fiber/v2/middleware/logger
Enter fullscreen mode Exit fullscreen mode

Import logger in our project

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger" // new
)
Enter fullscreen mode Exit fullscreen mode

and finally, connect this middleware with our app

app.Use(logger.New())
Enter fullscreen mode Exit fullscreen mode

Here is our final code :

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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

21:44:48 | 200 |      0s |       127.0.0.1 | GET     | /
Enter fullscreen mode Exit fullscreen mode

3. Decide Endpoints for our TODO app 🔗

localhost:8000/api/todos

request: GET
description: To get all todos
Enter fullscreen mode Exit fullscreen mode

localhost:8000/api/todos/:id

request: GET
description: Get todo by id
Enter fullscreen mode Exit fullscreen mode

localhost:8000/api/todos

request: POST
input: {
    title : String
}
description: Create new todo
Enter fullscreen mode Exit fullscreen mode

localhost:8000/api/todos/:id

request: PUT
input: {
    title : String,
    completed : Boolean
}
description: Update todo
Enter fullscreen mode Exit fullscreen mode

localhost:8000/api/todos/:id

request: DELETE
description: Delete todo
Enter fullscreen mode Exit fullscreen mode

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.

package controllers

import (
    "github.com/gofiber/fiber/v2"
)
Enter fullscreen mode Exit fullscreen mode

Add Todo structure

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

Let's add some predefined todos

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

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

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

Here is our final code in controllers/todo.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,
        },
    })
}
Enter fullscreen mode Exit fullscreen mode

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

package routes

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

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

func TodoRoute(route fiber.Router) {
    route.Get("", controllers.GetTodos)
}
Enter fullscreen mode Exit fullscreen mode

Here is our final code in routes/todo.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)
}
Enter fullscreen mode Exit fullscreen mode

Step 3 :

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

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/devsmranjan/golang-fiber-basic-todo-app/routes" // new // replace
)
Enter fullscreen mode Exit fullscreen mode

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.

func setupRoutes(app *fiber.App) {}
Enter fullscreen mode Exit fullscreen mode

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

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 😉",
        })
    })
}
Enter fullscreen mode Exit fullscreen mode

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

setupRoutes(app)
Enter fullscreen mode Exit fullscreen mode

Here is our final main() looks like.

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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

api := app.Group("/api")
Enter fullscreen mode Exit fullscreen mode

Add response for /api route

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

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

routes.TodoRoute(api.Group("/todos"))
Enter fullscreen mode Exit fullscreen mode

Final setupRoutes() method looks like :

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"))
}
Enter fullscreen mode Exit fullscreen mode

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

{
    "data": {
        "todos": [
            {
                "id": 1,
                "title": "Walk the dog 🦮",
                "completed": false
            },
            {
                "id": 2,
                "title": "Walk the cat 🐈",
                "completed": false
            }
        ]
    },
    "success": true
}
Enter fullscreen mode Exit fullscreen mode

Yeah

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 :

import (
    "fmt" // new
    "strconv" // new

    "github.com/gofiber/fiber/v2"
)
Enter fullscreen mode Exit fullscreen mode

Okay. Let's create a controller to 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,
        },
    })
}
Enter fullscreen mode Exit fullscreen mode

A controller to get a single todo by 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",
    })
}
Enter fullscreen mode Exit fullscreen mode

A controller to update a todo

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,
        },
    })
}
Enter fullscreen mode Exit fullscreen mode

A controller to delete a todo

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",
    })
}
Enter fullscreen mode Exit fullscreen mode

Here is our final code in controllers/todo.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",
    })
}
Enter fullscreen mode Exit fullscreen mode

6. Create other routes 🚏

Let's create other routes for our todo app.

Now, inside TodoRoute() add all below routes

route.Post("", controllers.CreateTodo)
route.Put("/:id", controllers.UpdateTodo)
route.Delete("/:id", controllers.DeleteTodo)
route.Get("/:id", controllers.GetTodo)
Enter fullscreen mode Exit fullscreen mode

Finally, our final code looks like :

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
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

Delete todo

Congooooooo 🥳 🥳 🥳 We did it 💪🏻

Did it

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 :)

Top comments (6)

Collapse
 
robmeijeren profile image
Rob Meijeren

The thing that I always miss with this kind of explanations are the middle ware to secure the endpoints as now everyone can call them and create havoc

Collapse
 
devsmranjan profile image
Smruti Ranjan Rana

This is the first part of the Golang for Web series. Here I want to show, how this fiber web framework works with Golang.
We will go step by step. We'll add a database in our next part of this series with this app and then we will go for Authentication to make our endpoints private and so on.
Stay tuned. 🤝

Collapse
 
antonkusnadi profile image
Anton

Thank you for posting this. Any idea when will you post the second part?

Collapse
 
p4tin profile image
Paul Fortin • Edited

Well written with nice explanations... Seems very similar to the echo router that I have been using are you aware of why I would choose fiber over echo?

Collapse
 
devsmranjan profile image
Smruti Ranjan Rana

I've not tried echo yet. But if you'll check the benchmark score of fiber, it seems pretty good than echo.
Take a look here - docs.gofiber.io/extra/benchmarks

Collapse
 
rohitkuwar profile image
RohitKuwar

Thank you so much for posting this blog about Fiber. This is very helpful. In my project I am using Firestore. Can you please guide me how can I connect to Firestore?