The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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.
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.
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:
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.
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 methods — GET, 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.
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.
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.
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:

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:

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

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. 🙂
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
6 Replies to "Building an Express-style API in Go with Fiber"
hi, how do you hot reload the go fiber server while developing the same way we use nodemon in nodejs ? thanks
you can use air, which hot compiles and reloads. go is so fast to compile it is a viable option.
fiber is compiler language, can’t hot reload
Nope, because Golang is compiler language, must be restart service
Hopefully not many people will take this as good practice: aka a global pointer to a db connection that’s going to be directly shared (and potentially changed) across goroutines.
Ah, the only issue that remains to be explained in your most excellent series of articles on Go is how to integrate LogRocket into a Go (backend) application! 🤓