DEV Community

Cover image for Go Fiber: Start Building RESTful APIs on Golang (Feat. GORM)
Gaurav Mehra
Gaurav Mehra

Posted on • Updated on

Go Fiber: Start Building RESTful APIs on Golang (Feat. GORM)

📚 Contents

Let's learn tech in plain english

If you are better with code than with text, feel free to directly jump to completed project, fork it and start adding your code -

GitHub logo percoguru / tutorial-notes-api-go-fiber

Build a RESTful API in Go: feat Fiber, Postgres, GORM

👷 What are we building ↑

We will be building a simple Note Taking API in Go to setup a basic repo which can be used to scale and create a complete backend service. The structure we create will help you understand and get started with APIs in Go.
If you have wondered how to start developing APIs in Go or you just completed understanding basics of Go and want to get started with real development this will be a great starting point.

🚅 Go ↑

Go has been around for more than a decade now. It is super loved and super fun to learn and work with.
Fully fledged full stack web applications can be built using Go, but for the purpose of this article which is to learn to build a basic web API in GoLang, we will stick to APIs.

⏩ Express and Fiber ↑

NodeJs has enjoyed a lot of love from people building backend services for the past decade.
Developing APIs the Express way is super developer friendly for both polished NodeJs developers and new Go Developers. Thus Fiber was built with Express in mind.
Fiber is a web framework for Go with APIs readiness from scratch, middleware support and super fast performance. We will be making use of Fiber to create our web API.

📁 Final Project Structure

Let's first look at what our final structure will look like -

notes-api-fiber
|
- config
|   - config.go
|
- database
|   - connect.go
|   - database.go
|
- internal
|   |     
|   - handler
|   |     - note
|   |         - noteHandler.go
|   |     
|   - model 
|   |     - model.go
|   |
|   - routes
|         - note
|             - noteRoutes.go
|
- router
|   - router.go
|
- main.go
- go.mod
- go.sum

Enter fullscreen mode Exit fullscreen mode

No need to worry, we will start building with just a single file and reach this state with sufficient logic and explanations on the way.

🎒 Basics of packages -

Go code is distributed in packages and we will be making a lot of them. Go packages are used for distribution of code and logic based on their usage. It can also be observed in the directory structure we draw above.

  • We declare the package name for a file by writing package <package_name> at the top.
  • In simple words packages are group of code in a go project that share the variables and functions.
  • Any number of files part of the same package share variables, functions, types, interfaces, basically any definition.
  • Inorder to reach any code for a package correctly all files for a package should be present in a single directory.

💡 Let's Get Started With APIs ↑

We will be beginning with a single file, the starting point of our code - main.go. Create this file in the root directory of our project.

main.go ↑

Let's start with writing the root file, the main.go file. This will be the starting point of the application. Right now, we will just be initializing a fiber app inside here. We will add more things later and this will become the place where the setup happens.

main.go
package main

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

func main() {
    // Start a new fiber app
    app := fiber.New()

    // Listen on PORT 300
    app.Listen(":3000")
}

Enter fullscreen mode Exit fullscreen mode

Let's create an endpoint ↑

To have a basic understanding of how an API endpoint is created, let's first create a dummy endpoint to get started.

If you have worked with Express you might notice the resemblance, if you have not worked with Express the resemblance will come other way around for you

main.go
package main

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

func main() {
    // Start a new fiber app
    app := fiber.New()

    // Send a string back for GET calls to the endpoint "/"
    app.Get("/", func(c *fiber.Ctx) error {
        err := c.SendString("And the API is UP!")
        return err
    })

    // Listen on PORT 3000
    app.Listen(":3000")
}
Enter fullscreen mode Exit fullscreen mode

Run the server by running

go run main.go
Enter fullscreen mode Exit fullscreen mode

in the root directory. Then go to localhost:3000. You will see a page like this -
Basic API with Go Fiber

Now, that we have seen that how we can startup an API from a single file, containing a few lines of code.
Note that you can keep on adding more and more Endpoints in here and scale. Such scenario will come up many times during this article, but instead of scaling vertically we will try to distribute our code horizontally wherever possible

Now our project directory looks like -

notes-api-fiber
|
- main.go

Enter fullscreen mode Exit fullscreen mode

💼 Go modules ↑

Our project will become a Go Module. To know more about Go Modules visit https://golang.org/ref/mod. To start a Go module within our project directory run the command

go mod init <your_module_url>
Enter fullscreen mode Exit fullscreen mode

Normally <your_module_url> is represented by where your module will be published. For now you can use your Github profile. Eg - If your github username is percoguru, you will run

go mod init github.com/percoguru/notes-api-fiber
Enter fullscreen mode Exit fullscreen mode

The last part of your path is the name of your project.
Now any package that you create will be a subdirectory within this module. Eg a package foo would be imported by another package inside your module as github.com/percoguru/notes-api-fiber/foo.

Once you run the command, a file go.mod will be created that contains the basic information about our module and the dependencies that we would be adding. This can be considered as the package.json equivalent for Go.

Your go.mod will look like this -

go.mod
module github.com/percoguru/notes-api-fiber

go 1.17
Enter fullscreen mode Exit fullscreen mode

📐 Setup Essentials ↑

Now we will set up some basic stuff to support our APIs -

  • Makefile
  • Database (PostgreSQL)
  • Models
  • Environment Variables

Makefile ↑

As we introduce more and more changes, we will need to run go run main.go every time we want changes to reflect on our running server. To enable hot reload of our server create a Makefile in the root of your directory.

  • Install reflex
go install github.com/cespare/reflex@latest
Enter fullscreen mode Exit fullscreen mode
  • Add commands to your Makefile
build:
    go build -o server main.go

run: build
    ./server

watch:
    reflex -s -r '\.go$$' make run
Enter fullscreen mode Exit fullscreen mode
  • Run
make watch
Enter fullscreen mode Exit fullscreen mode
  • Make any changes to your code and see the server reloading in the terminal.

💾 Database ↑

We will be using Postgres for the database implementation in this article.

Although we are making just a Note Taking Application here, the purpose of this article is to allow you to scale from here. I would even encourage to go crazy with the database schema now only and add new entities or use something other than Notes. SQL is great for such scaling and you would not be exploring other options if you decide to scale further from here.

Get Postgres running on your machine, and create a database fiber-app for our implementation - follow instructions at - https://www.postgresql.org/download/

⚙️ Adding Config ↑

We will be adding an environment variable file .env in the root of our project directory. When we connect to a database we will require some variables, we will store those in this file.

.env
DB_HOST= localhost
DB_NAME= fiber-app
DB_USER= postgres
DB_PASSWORD= postgres
DB_PORT= 5432
Enter fullscreen mode Exit fullscreen mode

The above values will most probably remain the same for you too except the password, which will be the password you choose for the postgres user. Remember to create a database fiber-app before going ahead.
Now we have to pick up these variables from the .env file. For this purpose we will create a package config that will provide us the configurations for the project.
Create a folder config in the root of your directory and create a file config.go inside this.
First run -

go get github.com/joho/godotenv
Enter fullscreen mode Exit fullscreen mode
config.go
package config

import (
    "fmt"
    "os"

    "github.com/joho/godotenv"
)

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

Enter fullscreen mode Exit fullscreen mode

🔄 Connecting to the Database ↑

Inside the root folder of our project, create a directory named database. All our code related to database connection and migrations will reside here. This will become a package related to our database connection related operations, let's name this package database.

We will be using an ORM (Object Relational Mapping) as a middleware between our Go code and SQL database. GORM would be our ORM of choice for this article. It supports Postgres, Associations, Hooks and one feature that will help us a lot initially - Auto Migrations.
Add gorm and postgres driver for gorm by running -

go get gorm.io/gorm
go get gorm.io/driver/postgres
Enter fullscreen mode Exit fullscreen mode
connect.go
package database

import (
    "fmt"
    "log"
    "strconv"

    "github.com/percoguru/notes-api-fiber/config"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

// Declare the variable for the database
var DB *gorm.DB

// ConnectDB connect to db
func ConnectDB() {
    var err error
    p := config.Config("DB_PORT")
    port, err := strconv.ParseUint(p, 10, 32)

    if err != nil {
        log.Println("Idiot")
    }

    // Connection URL to connect to Postgres Database
    dsn := 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"))
    // Connect to the DB and initialize the DB variable
    DB, err = gorm.Open(postgres.Open(dsn))

    if err != nil {
        panic("failed to connect database")
    }

    fmt.Println("Connection Opened to Database")
}

Enter fullscreen mode Exit fullscreen mode

Notice how connect.go imports the package config. It looks for the package inside the folder ./config.

We have created the database connector, but when we run the application we run go run main.go. Right now we have not yet called the function connectDB(). We need to call this in order to connect to the database.
Let's go to our main.go file and connect to the database. We want to connect to our database whenever the server is run. So we can call the function connectDB() from the function main of the package main.

main.go
package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/percoguru/notes-api-fiber/database"
)

func main() {
    // Start a new fiber app
    app := fiber.New()

    // Connect to the Database
    database.ConnectDB()

    // Send a string back for GET calls to the endpoint "/"
    app.Get("/", func(c *fiber.Ctx) error {
        err := c.SendString("And the API is UP!")
        return err
    })

    // Listen on PORT 3000
    app.Listen(":3000")
}
Enter fullscreen mode Exit fullscreen mode

Now run

go run main.go
Enter fullscreen mode Exit fullscreen mode

And you are connected to the Database. You will see an output like this.
Connect To Database Go

We have not yet done any operations on the database. We will be creating models to represent the tables we want to store in the database.

🔖 Add Models

Create a folder internal in the root of your directory. All our internal logic (models, types, handlers, routes, constants etc.) will be stored in this folder.
Within this folder create a folder model and then the file model.go. Thus creating internal/model/model.go. The folder model will contain our package model.
We will be creating only one model for now - note, you can add more here or use something other than a note, like product, page, etc. Be sure to be creative. Even if you choose to go with some other model (which I am encouraging btw 😉) the other parts of code would remain mostly the same, so, don't hesitate to experiment.
Our Notes table would look like -

  • ID uuid
  • Title text
  • SubTitle text
  • Text text

To use uuid type in Go, run -

go get github.com/google/uuid
Enter fullscreen mode Exit fullscreen mode

To create this model, add this to the model.go file -

package model

import (
    "github.com/google/uuid"
    "gorm.io/gorm"
)

type Note struct {
    gorm.Model           // Adds some metadata fields to the table
    ID         uuid.UUID `gorm:"type:uuid"` // Explicitly specify the type to be uuid
    Title      string
    SubTitle   string
    Text       string
}
Enter fullscreen mode Exit fullscreen mode

Notice that in the line

ID         uuid.UUID `gorm:"type:uuid"`
Enter fullscreen mode Exit fullscreen mode

We are first telling Go that the type of this struct field is uuid.UUID and then telling GORM to create this column with type uuid by specifying with the tag gorm:"type:uuid"

🔁 Auto Migrations

GORM supports auto migrations, thus whenever you make changes to your model structs (like add column, change type, add index) and restart the server the changes will be reflected in the database automatically.

Note that to save you from accidental loss of data GORM does not delete columns automatically through migrations if you do so in your struct. Though you can configure GORM to do so.

Go into your database/connect.go file and add in a line to automigrate after connecting to the database

...
    // Connect to the DB and initialize the DB variable
    DB, err = gorm.Open(postgres.Open(dsn))

    if err != nil {
        panic("failed to connect database")
    }

    fmt.Println("Connection Opened to Database")

    // Migrate the database
    DB.AutoMigrate(&model.Note{})
    fmt.Println("Database Migrated")
}
Enter fullscreen mode Exit fullscreen mode

Now, restart the server. And your database is migrated. You can verify the migration by going into postgres, on Linux you can use psql and on Windows pgAdmin or DBeaver.
On psql on Ubuntu -

  • Connect to the database notes-api
\c notes-api
Enter fullscreen mode Exit fullscreen mode
  • Get details about the notes table Note that GORM pluralizes the struct name to be the table name, also note in the below image how field names are handled against corresponding field names in struct
\d+ notes
Enter fullscreen mode Exit fullscreen mode

Output
Notes Table
The extra fields have been added by GORM, when we added gorm.Model at the top of our struct.
Now we have verified our migrations and our model is in the database. 😎

Let's re-look at our project structure now -

notes-api-fiber
|
- config
|   - config.go
|
- database
|   - connect.go
|
- internal
|   |     
|   |- model 
|   |     - model.go
|
- main.go
- go.mod
- go.sum
- .env

Enter fullscreen mode Exit fullscreen mode

🚡 Add The Routes ↑

Our API will have routes which are endpoints that the browser or a web or mobile application will use to perform CRUD operations on the data.
First we setup a basic router to start routing on the fiber app.
Create a folder router in the root directory of our project and inside it a file router.go.
Inside the file add the following code -

package router

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

func SetupRoutes(app *fiber.App) {

}
Enter fullscreen mode Exit fullscreen mode

We just declared a function SetupRoutes inside package router that takes a fiber app as an argument. This function will take a fiber app and route calls to this app to specific routes or route handlers.
APIs are grouped based on parameters and fiber allows us to do so. For example if there are three API endpoints -

  • GET api/user/:userId - Get User with userId
  • GET api/user - Get All Users
  • PUT api/user/:userId - Update User with userId

We do not have to write all the repeated parameters. What we can instead do is -

...
app := fiber.App()
api := app.Group("api")

user := api.Group("user")

user.GET("/", func(c *fiber.Ctx) {} )

user.GET("/:userId", func(c *fiber.Ctx) {} )

user.PUT("/:userId" ,func(c *fiber.Ctx) {} )
Enter fullscreen mode Exit fullscreen mode

This helps a lot when we scale and add a lot of API and complex routing.
Note that 'api' is of type fiber.Router and 'app' is of type fiber.App and these both have the function Group

🔨 Routes ↑

We want to keep our routes like SERVER_HOST/api/param1/param2. Thus we add this line to our function SetupRoutes -

api := app.Group("/api", logger.New()) // Group endpoints with param 'api' and log whenever this endpoint is hit.
Enter fullscreen mode Exit fullscreen mode

The handler logger.New() will also log all the API calls and their statuses.
Now as I had said earlier we would scale horizontally, we could have added routes related to notes in the main.go file itself, but we created a router package to handle API routes. Now, when the API will scale you will be adding a lot of models so we can not add add notes api in router package as well.
We will add specific router for each model and right now for Notes
Inside the internal folder create a folder routes this is where we will have subdirectories for all routes related to a model. Within the routes folder add a folder note and inside the folder add a file note.go.
You have created -

internal/routes/note/note.go
Enter fullscreen mode Exit fullscreen mode

Inside the file note.go add the following code -

package noteRoutes

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

func SetupNoteRoutes(router fiber.Router) {

}
Enter fullscreen mode Exit fullscreen mode

The function SetupNoteRoutes takes a fiber.Router and handles endpoints to the note model. Thus add the line -

note := router.Group("/note")
Enter fullscreen mode Exit fullscreen mode

We will be adding CRUD (Create, Read, Update, Delete) operations to the note routes. Thus -

package noteRoutes

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

func SetupNoteRoutes(router fiber.Router) {
    note := router.Group("/note")
    // Create a Note
    note.Post("/", func(c *fiber.Ctx) error {})
    // Read all Notes
    note.Get("/", func(c *fiber.Ctx) error {})
    // Read one Note
    note.Get("/:noteId", func(c *fiber.Ctx) error {})
    // Update one Note
    note.Put("/:noteId", func(c *fiber.Ctx) error {})
    // Delete one Note
    note.Delete("/:noteId", func(c *fiber.Ctx) error {})
}
Enter fullscreen mode Exit fullscreen mode

Note that we have to write handler function to do the tasks we have commented for all the API endpoints
We will be writing these handler in a seperate package

🔧 Handlers ↑

Handlers are functions that take in a Fiber Context (fiber.Ctx) and use the request, send the response or just act as a middleware and pass on the authority to the next handler.
To know more about handlers and middleware visit the Fiber Documentation
Inside the internal folder add a folder handlers this will contain all the API handlers with a specific sub directory for each model. So create a folder note within handlers and add a file note.go inside the note folder.
You just created -

internal/handlers/note/note.go
Enter fullscreen mode Exit fullscreen mode

Now we will add handlers that we require in routes/note/note.go file to handlers/note/note.go file.

handlers/note/note.go
package noteHandler
Enter fullscreen mode Exit fullscreen mode
  • Add the Read Notes handler -
func GetNotes(c *fiber.Ctx) error {
    db := database.DB
    var notes []model.Note

    // find all notes in the database
    db.Find(&notes)

    // If no note is present return an error
    if len(notes) == 0 {
        return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No notes present", "data": nil})
    }

    // Else return notes
    return c.JSON(fiber.Map{"status": "success", "message": "Notes Found", "data": notes})
}
Enter fullscreen mode Exit fullscreen mode
  • Add the Create Note handler -

func CreateNotes(c *fiber.Ctx) error {
    db := database.DB
    note := new(model.Note)

    // Store the body in the note and return error if encountered
    err := c.BodyParser(note)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Review your input", "data": err})
    }
    // Add a uuid to the note
    note.ID = uuid.New()
    // Create the Note and return error if encountered
    err = db.Create(&note).Error
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Could not create note", "data": err})
    }

    // Return the created note
    return c.JSON(fiber.Map{"status": "success", "message": "Created Note", "data": note})
}
Enter fullscreen mode Exit fullscreen mode
  • Add the Get Note Handler
func GetNote(c *fiber.Ctx) error {
    db := database.DB
    var note model.Note

    // Read the param noteId
    id := c.Params("noteId")

    // Find the note with the given Id
    db.Find(&note, "id = ?", id)

    // If no such note present return an error
    if note.ID == uuid.Nil {
        return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No note present", "data": nil})
    }

    // Return the note with the Id
    return c.JSON(fiber.Map{"status": "success", "message": "Notes Found", "data": note})
}
Enter fullscreen mode Exit fullscreen mode
  • Add the Update Note Handler
func UpdateNote(c *fiber.Ctx) error {
    type updateNote struct {
        Title    string `json:"title"`
        SubTitle string `json:"sub_title"`
        Text     string `json:"Text"`
    }
    db := database.DB
    var note model.Note

    // Read the param noteId
    id := c.Params("noteId")

    // Find the note with the given Id
    db.Find(&note, "id = ?", id)

    // If no such note present return an error
    if note.ID == uuid.Nil {
        return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No note present", "data": nil})
    }

    // Store the body containing the updated data and return error if encountered
    var updateNoteData updateNote
    err := c.BodyParser(&updateNoteData)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Review your input", "data": err})
    }

    // Edit the note
    note.Title = updateNoteData.Title
    note.SubTitle = updateNoteData.SubTitle
    note.Text = updateNoteData.Text

    // Save the Changes
    db.Save(&note)

    // Return the updated note
    return c.JSON(fiber.Map{"status": "success", "message": "Notes Found", "data": note})
}

Enter fullscreen mode Exit fullscreen mode
  • Add the Delete Note Handler
func DeleteNote(c *fiber.Ctx) error {
    db := database.DB
    var note model.Note

    // Read the param noteId
    id := c.Params("noteId")

    // Find the note with the given Id
    db.Find(&note, "id = ?", id)

    // If no such note present return an error
    if note.ID == uuid.Nil {
        return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No note present", "data": nil})
    }

    // Delete the note and return error if encountered
    err := db.Delete(&note, "id = ?", id).Error

    if err != nil {
        return c.Status(404).JSON(fiber.Map{"status": "error", "message": "Failed to delete note", "data": nil})
    }

    // Return success message
    return c.JSON(fiber.Map{"status": "success", "message": "Deleted Note"})
}

Enter fullscreen mode Exit fullscreen mode

📨 Connecting handlers to routes ↑

Add the handlers in the note routes, changing the file routes/note/note.go to -

routes/note/note.go
package noteRoutes

import (
    "github.com/gofiber/fiber/v2"
    noteHandler "github.com/percoguru/notes-api-fiber/internals/handlers/note"
)

func SetupNoteRoutes(router fiber.Router) {
    note := router.Group("/note")
    // Create a Note
    note.Post("/", noteHandler.CreateNotes)
    // Read all Notes
    note.Get("/", noteHandler.GetNotes)
    // // Read one Note
    note.Get("/:noteId", noteHandler.GetNote)
    // // Update one Note
    note.Put("/:noteId", noteHandler.UpdateNote)
    // // Delete one Note
    note.Delete("/:noteId", noteHandler.DeleteNote)
}

Enter fullscreen mode Exit fullscreen mode

Notice how noteHandler is imported because of the mismatch between the folder name and the package name, if you want to avoid this, name the folder as noteHandler too

📨 Setup Note Routes ↑

Setup the Note Routes in the router/router.go file, editing it to -

router/router.go
package router

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    noteRoutes "github.com/percoguru/notes-api-fiber/internals/routes/note"
)

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

        // Setup the Node Routes
    noteRoutes.SetupNoteRoutes(api)
}

Enter fullscreen mode Exit fullscreen mode

📨 Setup Router ↑

Until now we have created the router and used it to setup note routes, now we need to setup this router from the main function.
Inside the main.go remove the dummy endpoint we had created and add in the line -

router.setupRoutes(app)
Enter fullscreen mode Exit fullscreen mode

Converting your main.go into -

main.go
package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/percoguru/notes-api-fiber/database"
    "github.com/percoguru/notes-api-fiber/router"
)

func main() {
    // Start a new fiber app
    app := fiber.New()

    // Connect to the Database
    database.ConnectDB()

    // Setup the router
    router.SetupRoutes(app)

    // Listen on PORT 3000
    app.Listen(":3000")
}
Enter fullscreen mode Exit fullscreen mode

et voilà ! 💥 💰

We did it!

Run

make watch
Enter fullscreen mode Exit fullscreen mode

And try the endpoints out on Postman. And see it all working so very fine.

What's Next!!

Now you have built a web API from scratch in Go. You came across the nuances of Go, Fiber, GORM and Postgres. This has been a basic setup and you can grow your api into a full stack web application -

  • Add JWT based authentication
  • Add more models to your backend and play around with different data types
  • Add in a frontend using a UI framework like React or use Go Templates

Supporting Documentations and Resources -

Go is an amazing language to learn, play and work with. If you like what you built, make sure to be consistent with Go and you will surely fall in love with it. Every line of code that you wrote here has been written from scratch and there is no boilerplate.


🤘 Will be back with more stuff!!

Got some question, idea or feedback? Comment below, I will love to hear from you! 📣

Discussion (1)

Collapse
arthemis97 profile image
Tod-Od

Thank you :)