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

devsmranjan

Smruti Ranjan Rana

Posted on November 25, 2020

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

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
devsmranjan
Smruti Ranjan Rana

Posted on November 25, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About