Consider Go Fiber as a replacement for Express.js

May 10, 2020

Recently there another new Golang web framework called Fiber that announced to be very similar to Express.js with better performance. Let’s take a look at it 😄

my first blog post

Disclaimer: I’m a long time Express.js developer. I know that Express.js, or Node.js in general, have some weaknesses in performance. After looking around for an alternative framework, I discovered Go Fiber. Through this article, I want to share my experience when I tried to switching from Express.js to Go Fiber.

1. Project setup

We, Express.js users, often use express-generator to generate Express.js app, so the app.js from express-generator (v4.16.1) is too familiar to us:

app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

We have a view engine setup. The express-generator prepares necessary middlewares for us include logger middleware, body-parser middleware, cookie parser middleware, static path middleware. We also have an index router endpoint, users router endpoint, and not found error endpoint.

Let’s check those declaration syntaxes in Go Fiber:

main.go
package main

import (
    "gofiber/starter/routes"

    "github.com/gofiber/fiber"
    "github.com/gofiber/logger"
    "github.com/gofiber/template"
)

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

    // view engine setup
    app.Settings.TemplateFolder = "./views"
    app.Settings.TemplateEngine = template.Pug()
    app.Settings.TemplateExtension = ".jade"

    app.Use(logger.New())
    app.Static("/", "./public")

    routes.SetupIndexRouter(app.Group("/"))
    routes.SetupUsersRouter(app.Group("/users"))

    // catch 404 error handler
    app.Use("/*", func(c *fiber.Ctx) {
        bind := fiber.Map{
            "title":   "Page not found",
            "message": "Page not found",
            "error": map[string]interface{}{
                "status": 400,
                "stack":  "",
            },
        }
        // render the error page
        if err := c.Render("./error", bind); err != nil {
            c.Status(500).Send(err.Error())
        }
    })

    app.Listen(3000)
}

Are you amazed or a little confused? However, because you are still new to Golang, some syntax may make you worry 😄. Let me explain some of them.

You may wonder where is the routes variable come from and why all the method in the main function is capitalized. Well, actually, routes is a package in this project import through this

import “gofiber/starter/routes”

A lot of times in Golang you will meet capitalized methods. They are public methods in a package that you can call them by using [package-name].[capitalized-method], in this case,

routes.SetupIndexRouter(app.Group(/))
routes.SetupUsersRouter(app.Group(/users”))

Back to the Go Fiber project, after understanding the syntax, you may think the declaration is almost the same, right? Go Fiber has a similar way to declare view engine setup, logger, static folder path configure, and catch-all routes to return 404 not found page.

Moreover, you can group endpoint (i.e “/” and “/users” in the example), and then you can define the router handler function for each HTTP method call to that route. Convenience, eh!?

However, you may notice that there are no cookies parser and response body-parser in this Express Golang version because it is implemented inside the framework.

Now I can say there are 80% similar in this comparison, right? Ok, let move on more advanced things 😄.

2. Route Parameters

This example I took from Express.js document website, there are 2 params in this route: userId and bookId

/*
  Route path: /users/:userId/books/:bookId
  Request URL: http://localhost:3000/users/34/books/8989
  req.params: { "userId": "34", "bookId": "8989" }
*/
app.get('/users/:userId/books/:bookId', function (req, res) {
  res.send(req.params)
})

This is its equivalent implement in Go Fiber

/*
  Route path: /users/:userId/books/:bookId
  Request URL: http://localhost:3000/users/34/books/8989
  req.params: { "userId": "34", "bookId": "8989" }
*/
app.Get("/users/:userId/books/:bookId", func(c *fiber.Ctx) {
  c.JSON(fiber.Map{
    "userId": c.Params("userId"),
    "bookId": c.Params("bookId"),
  })
})

A little more syntax because Golang is not a dynamic language like Javascript. But those syntaxes are still neat, right?

3. Chained Middlewares

What made me feel comfortable when developing with Express.js is the way we can intercept to the API Handler through Middleware. Middleware is defined in Express.js as below

***Middleware*** functions are functions that have access to the [request object](http://expressjs.com/en/4x/api.html#req) (req), the [response object](http://expressjs.com/en/4x/api.html#res) (res), and the next function in the application’s request-response cycle. The next function is a function in the Express router which, when invoked, executes the middleware succeeding the current middleware.

We can apply as many middlewares to any endpoints. For example, we need to add logic for authenticating and authorizing before running an endpoint handler. The way we implement them in Express.js is to add those middleware methods before the handler method. In each of the middleware methods, we want to pass the results to the request object.

function authenticate(req, res, next) {
  if (req.params.status === "authenticated") {
    req.isAuthenticated = true
  } else {
    req.isAuthenticated = false
  }
  next();
}


function authorize(req, res, next) {
  if (req.isAuthenticated == false) {
    next()
    return
  }
  if (req.params.role === "admin") {
    req.redirectRoute = "dashboard"
  } else if (req.params.role === "user") {
    req.redirectRoute = `homepage/${req.params.userId}`
  } else {
    req.redirectRoute = "contact-support"
  }
  next();
}


app.get('/verify/:status/:role/:userId', authenticate, authorize, function (req, res) {
  if(req.isAuthenticated == false){
    res.status(403);
    res.send('Unauthenticated. Please signup!');
    return 
  } 
  res.send('Redirecting ' + req.redirectRoute);
});

Interestingly, we can do the same in Go Fiber

func main() {
  // initialization codes
  
  app.Get("/verify/:status/:role/:userId", authenticate, authorize, func(c *fiber.Ctx) {
        if c.Locals("isAuthenticated") == false {
            c.Status(403)
            c.Send("Unauthenticated. Please signup!")
            return
        }
        c.Send("Redirecting " + c.Locals("redirectRoute").(string))
    })
  
  // some other codes
}

func authenticate(c *fiber.Ctx) {
    if c.Params("status") == "authenticated" {
        c.Locals("isAuthenticated", true)
    } else {
        c.Locals("isAuthenticated", false)
    }
    c.Next()
}

func authorize(c *fiber.Ctx) {
    if c.Locals("isAuthenticated") == false {
        c.Next()
        return
    }
    if c.Params("role") == "admin" {
        c.Locals("redirectRoute", "dashboard")
    } else if c.Params("role") == "user" {
        c.Locals("redirectRoute", "homepage/"+c.Params("userId"))
    } else {
        c.Locals("redirectRoute", "contact-support")
    }
    c.Next()
}

As you can see, the syntax is again about 80% similar, again 👍!

4. Live reload

Ok, so this is not a part of Express.js framework or Go Fiber framework but I know that Express.js users use it a lot. The most popular live reload tool for Nodejs projects is Nodemon but in Golang, there is actually a similar tool call Air

Their idea about live reload are similar. If in Nodemon, we can config nodemon.json to ignore watching folders and file and reload delay time.

nodemon.json
{
    "ignore": [
      ".git",
      "node_modules/**/node_modules"
    ],
    "delay": "500"
}

In Air, we can configure the same configuration in .air.conf

root = "."
tmp_dir = "tmp"

[build]
delay = 500 # ms

This is the simplest configuration. You can check their doc for advanced configurations that serve your needs

5. Conclusion

Below are Express.js project and Go Fiber project that include all the code in this article, so you can compare and acknowledge them by yourself.

vo9312/expressjs-starter

vo9312/gofiber-starter

The purpose of this article is to highlight the similarity between the two frameworks. If you want to check about how efficient Go Fiber is compared to Express.js, please check this benchmark. In reality, the benchmark may different but overall, Go Fiber performs better.

With this similarity in syntaxes, I believe that Express.js developers will have a seamless experience when switch to Go Fiber. However, you still have to know the fundamental first, I suggest that you take a tour of go before trying Go Fiber 😁.

To me, Express.js still great, I still love it a lot. However this Go Fiber is more suitable when I seriously think about backend performance. I hope that you will have a great experience with this new Express.js, just like me 😁.