Alexander Nnakwue Software Engineer. React, Node.js, Python, and other developer tools and libraries.

Building an Express-style API in Go with Fiber

11 min read 3189

Building An Express-style API In Go With Fiber

Introduction

If you are familiar with Express, you may recognize that Fiber is inspired by the awesome Node.js framework — except it is written in Go. Why?

Well, because Go is very fast, low on memory footprint, and highly performant for building scalable web servers and applications.

Fiber leverages these performance benefits and features. Firstly, it is based on the fasthttp package, which is the fastest HTTP client library in the Go ecosystem. From benchmark results, fasthttp is 10 times as fast as the net/http native Go client package.

In this post, we are going to explore Fiber by looking at its features and components, such as routing, middleware support, and context. At the end of the day, we should then be able to apply these features and build a demo application that interacts with a database of our choice.

Prerequisites

To easily follow along with this tutorial, we should have at least a basic knowledge of the Go programming language. It might also be beneficial to know a little bit of Express, as this could help in quickly understanding Fiber from an architecture point of view.

Also, make sure you have the Postgres.app for your OS of choice — you can download it here. Also, you can install any GUI client for Postgres. In this article, we will be using Postico, which you can download here.

Finally, make sure you have the latest version of Go installed on your machine. Instructions to do so can be found in the documentation.

In the coming section, we will talk briefly about the motivation behind Fiber. Let’s go.

Motivation behind Go Fiber

As we mentioned earlier, Fiber was inspired by Express and takes on almost the same design and thinking. For example, this is a simple Fiber app:

package main 

import "github.com/gofiber/fiber"

func main() { 
  // Fiber instance
  app := fiber.New()  

  // Routes
  app.Get("/", hello)

  // start server
  app.Listen(3000) 
}

  // Handler
  func hello(c *fiber.Ctx){
    c.send("Hello, world!")
  }

// Note: we can pass any other native listerner using the Serve method.

And this is a simple Express app:

We made a custom demo for .
No really. Click here to check it out.

const express = require('express') 

// Express instance
const app = express()

// Routes
app.get('/', hello)

// Start server
app.listen(3000)

// Handler
function hello(req, res) { 
  res.send('hello world!')
})

Just like Express, this simple Fiber app above mirrors just the bare minimum needed to start a simple server. A really interesting feature is the use of the fasthttp RequestCtx package, which basically helps with handling regular HTTP requests and responses, with all the methods we already know: req.query, req.params, req.body, and so on.

Note that to run the above application in our development machines, all we need to do is make sure we have Go installed. After that, we can go ahead and create a new Go module:

go init github.com/firebase007/go-rest-api

Now we can go ahead and create a file in the root directly — let’s call it sample.go. Then, we can paste the code above into the file we just created and run the go run sample.go command to start our program. The output is shown below:

retina@alex go-rest-api % go mod init github.com/firebase007/go-rest-api-with-fiber
go: creating new go.mod: module github.com/firebase007/go-rest-api-with-fiber
retina@alex go-rest-api % go get -u github.com/gofiber/fiber                       
go: finding github.com/gofiber/fiber v1.9.6
go: downloading github.com/gofiber/fiber v1.9.6
go: extracting github.com/gofiber/fiber v1.9.6
go: updates to go.mod needed, but contents have changed
retina@alex go-rest-api % go run sample.go                                         
        _______ __
  ____ / ____(_) /_  ___  _____
_____ / /_  / / __ \/ _ \/ ___/
  __ / __/ / / /_/ /  __/ /
    /_/   /_/_.___/\___/_/ v1.9.6
Started listening on 0.0.0.0:3000

Note: Let’s not forget to import the Fiber package into our workspace. To do so, we can run:

go get -u github.com/gofiber/fiber

After these steps above, we can visit our browser on port 3000 to see that our app works. The browser renders the output shown below:

Hello, World!

Remember that after importing the Fiber package, app := fiber.New() basically calls the New function located in the app.go file. This function accepts a pointer of optional settings we can pass as arguments to our app on initialization. We can also look at how the newServer method initializes the fasthttp server on this line.

It is great to point out that Fiber is quickly becoming very popular as a framework for building web servers and applications in Go. It is gradually gaining huge momentum and traction from the Go community and developers alike for their APIs, and also for Node.js developers moving to Go.

As can be seen from the above example, it is quite easy and fast to create a simple Fiber app, just like Express. Let’s now learn more about Fiber by exploring its major component features and how they are implemented in Go.

Fiber’s component features

Routing

Just like Express, Fiber comes with a highly performant router, which, like the Express router, has a callback function that runs for every request that matches a specific path on our server. Let’s see the signature:

// Function signature
app.Method(path string, ...func(*fiber.Ctx))

Note that Method represents regular HTTP methodsGET, POST, HEAD, PUT and so on. Path represents the route we intend to match, and ...func(*fiber.Ctx) represents a handler or callback that runs for that particular route. It is also important to note that we can have multiple handlers for a particular route, useful mainly when we intend to pass middleware functions for any purpose we intend.

As always, app is an instance of a Fiber app. To serve static files, we can use the app.Static() method. More details about routing in Fiber can be found in the docs. The implementation can be found in the Layer.go, router.go, and app.go files in the GitHub repo.

Note: We can think of a route as one big ordered slice. When a request comes in, the first handler that matches the method name, path, and pattern would be executed. Also, based on the route matched at any particular time, we tend to know which middleware would be executed next.

Middleware support

Fiber already comes with some prebuilt middleware. Just as a recap, a middleware helps intercept and manipulate requests just before they get to a main handler or controller. Middleware functions are basically part of the request cycle/context, usually for performing certain actions.

Let’s see a very simple middleware example of a 404-handler from the Go Fiber Recipes repo on GitHub:

package main

import "github.com/gofiber/fiber"

// handler function 
func handler() func(*fiber.Ctx) {
        return func(c *fiber.Ctx) {
                c.Send("This is a dummy route")
        }
}

func main() {
        // Create new Fiber instance
        app := fiber.New()

        // Create new sample GET routes
        app.Get("/demo", handler())
        app.Get("/list", handler())

        // Last middleware to match anything
        app.Use(func(c *fiber.Ctx) {
                c.SendStatus(404) // => 404 "Not Found"
        })

        // Start server on http://localhost:3000
        app.Listen(3000)
}

This is a very simple usage of a middleware. In the above example, the middleware checks for routes that don’t match the ones registered. Just like Express, we can see that it is the last thing registered for our app with the app.Use() method. Note that if we navigate to a route that isn’t /demo or list on the browser, we will get the error Not Found.

The signature of the Use method, which registers a middleware route, is shown below:

func (*fiber.App).Use(args ...interface{}) *fiber.App

This signifies an instance of a Fiber app, with the Use method that accepts an empty interface as an argument. Again, a middleware would match a request beginning with the provided prefix, and if none is provided, it defaults to "/". Finally, there are a bunch of other middleware functions available in this section of the documentation. You can check them out to learn more.

Context

As we mentioned earlier, context holds the HTTP request and response, with methods for request query, parameters, body, and so on. The most basic example we can quickly relate with is using the Body method — just like when we do req.body in Express.

In Fiber, the signature for the context Body method is shown below:

c.Body() string // type string

Here’s a simple use case:

// curl -X POST http://localhost:8080 -d user=john
app.Post("/", func(c *fiber.Ctx) {
// Get raw body from POST request
c.Body() // user=john
})

More details about other methods available in the context package can be found here in the docs.

By now, we have explored how routing works in Fiber, and we have also looked at the middleware support and context. Now let’s use all these features and work our way through building a Fiber application that interacts with a database.

Building a demo application with Fiber

In this section, we will explore our own way of structuring a scalable Fiber application and, in the process, learn about implementing Fiber’s core features. In this demo, we will be making use of the pq package, which is a pure Go Postgres driver for the database/sql package. We can check it our here on Go’s package repository.

Also, we will be making use of two middleware packages, basicauth and logger, which are part of Fiber’s supported inbuilt middleware. To begin, we need to initialize a new Go module with the following command:

go init github.com/firebase007/go-rest-api-with-fiber

Then we can go ahead and install the following packages using the go get command. At the end of the day, our go.mod file should look like this:

module github.com/firebase007/go-rest-api-with-fiber

go 1.13

require (
    github.com/gofiber/basicauth v0.0.3
    github.com/gofiber/fiber v1.9.6
    github.com/gofiber/logger v0.0.8
    github.com/joho/godotenv v1.3.0
    github.com/klauspost/compress v1.10.5 // indirect
    github.com/lib/pq v1.5.2
)

Now we are ready to start a new Fiber project. After navigating into our module directory, we can go ahead and create a main.go file in the root path. Here’s how it should look:

package main 

import (
"github.com/gofiber/fiber" // import the fiber package
"log"
"github.com/gofiber/fiber/middleware"
"github.com/firebase007/go-rest-api-with-fiber/database"
"github.com/firebase007/go-rest-api-with-fiber/router"

_ "github.com/lib/pq"
)

  // entry point to our program
func main() { 
  // Connect to database
  if err := database.Connect(); err != nil {
      log.Fatal(err)
    }

 // call the New() method - used to instantiate a new Fiber App
  app := fiber.New()

  // Middleware
  app.Use(middleware.Logger())

  router.SetupRoutes(app)

  // listen on port 3000
  app.Listen(3000) 

}

Here, we are importing the Fiber package and two other packages we have created inside our project directory: router and database. Before we proceed, here is a screenshot of our project directory:

Folder Structure For Our API
Folder structure for our API.

In the main function, we have instantiated the Connect function from the database package. The contents of our database package are shown below:

package database
import (
    "database/sql"
    "fmt"
    "strconv"
    "github.com/firebase007/go-rest-api-with-fiber/config" 
)

// Database instance
var DB *sql.DB

// Connect function
func Connect() error {
    var err error
    p := config.Config("DB_PORT")
    // because our config function returns a string, we are parsing our str to int here 
    port,err := strconv.ParseUint(p, 10, 32) 
    if err != nil {
        fmt.Println("Error parsing str to int")
    }
    DB, err = sql.Open("postgres", fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", config.Config("DB_HOST"), port, config.Config("DB_USER"), config.Config("DB_PASSWORD"), config.Config("DB_NAME")))

    if err != nil {
        return err
    }
    if err = DB.Ping(); err != nil {
        return err
    }
    CreateProductTable()
    fmt.Println("Connection Opened to Database")
    return nil
}

It exports a single method that connects to our SQL database using the pg driver package. Note that after we are successfully connected to our database, we are calling a CreateProductTable() function, which, as the name implies, creates a new database table for us. The content of the file schema.go, still in our database package, is shown below:

package database

// CreateProductTable ...
func CreateProductTable() {
    DB.Query(`CREATE TABLE IF NOT EXISTS products (
    id SERIAL PRIMARY KEY,
    amount integer,
    name text UNIQUE,
    description text,
    category text NOT NULL
)
`)
}

Note that this function helps to create a new table in our database (if it doesn’t already exist). Earlier in our database file, we imported the config package, which takes care of returning env values based on their respective keys. The content of that file is shown below:

package config

import (
    "github.com/joho/godotenv"
    "os"
    "fmt"
)

// Config func to get env value from key ---
func Config(key string) string{
    // load .env file
    err := godotenv.Load(".env")
    if err != nil {
        fmt.Print("Error loading .env file")
    }
    return os.Getenv(key)

}

The sample.env file contains our secrets required for our database connection, as well as the username and password secret keys required for our basic-auth middleware package (for authenticating our routes). You can see its contents below:

DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=
DB_NAME=
USERNAME=
PASSWORD=

After we are done with the setup and connecting to our database, we can see that we are also importing and initializing the SetupRoutes function in our main package. This function helps with setting up our routes. The content of the router package is shown below:

package router

import (
    "github.com/firebase007/go-rest-api-with-fiber/handler"
    "github.com/firebase007/go-rest-api-with-fiber/middleware"
    "github.com/gofiber/fiber"
)

// SetupRoutes func
func SetupRoutes (app *fiber.App) { 
    // Middleware
    api := app.Group("/api", logger.New(), middleware.AuthReq())  

    // routes
    api.Get("/", handler.GetAllProducts)
    api.Get("/:id", handler.GetSingleProduct)
    api.Post("/", handler.CreateProduct)
    api.Delete("/:id", handler.DeleteProduct)
}

As we can see from the package file above, we are importing two packages: the handler and middleware packages. The middleware package contains an AuthReq function that returns a basic-auth config. The content of the package is shown below:

package middleware
import (
    "github.com/gofiber/fiber"
    "github.com/gofiber/basicauth"
    "github.com/firebase007/go-rest-api-with-fiber/config"
)

// AuthReq middleware
func AuthReq() func(*fiber.Ctx) {
    cfg := basicauth.Config{
        Users: map[string]string{
          config.Config("USERNAME"): config.Config("PASSWORD"),
        },
      }
    err := basicauth.New(cfg);
    return err
}

Note that the app.Group() method is used for grouping routes by creating a *Group struct. The signature is shown below:

app.Group(prefix string, handlers ...func(*Ctx)) *Group

From the routes file above, we are also calling our handler package, which contains functions that will be called when a route matches an appropriate path. The content of the handler package is shown below:

package handler

import (
    "log"
    "database/sql"
    "github.com/gofiber/fiber"
    "github.com/firebase007/go-rest-api-with-fiber/model"
    "github.com/firebase007/go-rest-api-with-fiber/database"
)

// GetAllProducts from db
func GetAllProducts(c *fiber.Ctx) {
    // query product table in the database
    rows, err := database.DB.Query("SELECT name, description, category, amount FROM products order by name")
    if err != nil {
        c.Status(500).JSON(&fiber.Map{
            "success": false,
            "error": err,
          })
        return
    }
    defer rows.Close()
    result := model.Products{}
    for rows.Next() {
        product := model.Product{}
        err := rows.Scan(&product.Name, &product.Description, &product.Category, &product.Amount)
        // Exit if we get an error
        if err != nil {
            c.Status(500).JSON(&fiber.Map{
                "success": false,
                "error": err,
              })
            return
        }
        // Append Product to Products
        result.Products = append(result.Products, product)
    }
    // Return Products in JSON format
    if err := c.JSON(&fiber.Map{
        "success": true,
        "product":  result,
        "message": "All product returned successfully",
      }); err != nil {
        c.Status(500).JSON(&fiber.Map{
            "success": false,
            "message": err,
          })
        return
    }
}

// GetSingleProduct from db
func GetSingleProduct(c *fiber.Ctx) {
    id := c.Params("id")
    product := model.Product{}
    // query product database
    row, err := database.DB.Query("SELECT * FROM products WHERE id = $1", id)
    if err != nil {
        c.Status(500).JSON(&fiber.Map{
            "success": false,
            "message": err,
          })
        return
    }
    defer row.Close()
    // iterate through the values of the row
    for row.Next() {
    switch err := row.Scan(&id, &product.Amount, &product.Name, &product.Description, &product.Category ); err {
        case sql.ErrNoRows:
              log.Println("No rows were returned!")
              c.Status(500).JSON(&fiber.Map{
                "success": false,
                "message": err,
              })
        case nil:
            log.Println(product.Name, product.Description, product.Category, product.Amount)
        default:
            //   panic(err)
              c.Status(500).JSON(&fiber.Map{
                "success": false,
                "message": err,
              })
    }
}

    // return product in JSON format
    if err := c.JSON(&fiber.Map{
        "success": false,
        "message": "Successfully fetched product",
        "product": product,
      }); err != nil {
        c.Status(500).JSON(&fiber.Map{
            "success": false,
            "message":  err,
          })
        return
    }

}

// CreateProduct handler
func CreateProduct(c *fiber.Ctx) {
    // Instantiate new Product struct
    p := new(model.Product)
    //  Parse body into product struct
    if err := c.BodyParser(p); err != nil {
        log.Println(err)
        c.Status(400).JSON(&fiber.Map{
            "success": false,
            "message": err,
          })
        return
    }
    // Insert Product into database
    res, err := database.DB.Query("INSERT INTO products (name, description, category, amount) VALUES ($1, $2, $3, $4)" , p.Name, p.Description, p.Category, p.Amount )
    if err != nil {
        c.Status(500).JSON(&fiber.Map{
            "success": false,
            "message": err,
          })
        return
    }
    // Print result
    log.Println(res)

    // Return Product in JSON format
    if err := c.JSON(&fiber.Map{
        "success": true,
        "message": "Product successfully created",
        "product": p,
      }); err != nil {
        c.Status(500).JSON(&fiber.Map{
            "success": false,
            "message":  "Error creating product",
          })
        return
    }
}

// DeleteProduct from db 
func DeleteProduct(c *fiber.Ctx) {
        id := c.Params("id")
        // query product table in database
        res, err := database.DB.Query("DELETE FROM products WHERE id = $1", id)
        if err != nil {
            c.Status(500).JSON(&fiber.Map{
                "success": false,
                "error": err,
              })
            return
        }
        // Print result
        log.Println(res)
        // return product in JSON format
        if err := c.JSON(&fiber.Map{
            "success": true,
            "message": "product deleted successfully",
          }); err != nil {
            c.Status(500).JSON(&fiber.Map{
                "success": false,
                "error": err,
              })
            return
        }
}

We are also importing our database and model packages from the handler package above. One thing to note is that Fiber comes with the fiber.Map() method, which is basically a shortcut for map[string]interface{}. More details about the project can be found on the GitHub repo.

To start the API, run go run main.go on the project root directory. Also, a POSTMAN collection is available if you intend to try out the endpoints for our API.

As an example, using POSTMAN to create a new product is shown below:

Creating A New Product From POSTMAN
Creating a new product from POSTMAN.

We can also visualize our database records with the newly created products using Postico, as below:

Showing Our Database Records With Postico
Showing our DB records with Postico.

Conclusion

Fiber is gaining some solid momentum and finding traction with both Go developers and Node.js developers moving to Go as a programming language.

As we have seen, Fiber is extremely easy to use — just like Express. It also comes with fasthttp methods under the hood, which gives it an edge in terms of performance. We have also explored some of Fiber’s most important features, which include support for middlewares (including third-party), just like Express.

Finally, Fiber is optimized for high-speed backend API development with Go. It offers support for static files, a prefork feature settings, templating engines, WebSockets, testing, and many more. The documentation is the best place to check out these awesome features.

Thanks again and please reach out to me on Twitter if you have any questions, or use the comment box below. Would be glad to answer them. 🙂

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    : Full visibility into your web apps

    LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

    In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

    .
    Alexander Nnakwue Software Engineer. React, Node.js, Python, and other developer tools and libraries.

    Leave a Reply