Build a Go REST API with Fiber and MongoDB

devwithmike

Dev Mike

Posted on May 25, 2021

Build a Go REST API with Fiber and MongoDB

In this tutorial, we will create a Movie Catchphrase API that allows you to Create, Read, Update and Delete Catchphrases, or in short perform CRUD operations.

We are going to use Fiber (a Go web framework) and the go mongo driver in order to interact with the MongoDB instance.

For this project, I assume you have already installed go onto your computer. You can verify the installation by running the following command in the terminal:

go
Enter fullscreen mode Exit fullscreen mode

If you haven't installed it yet, you can download the necessary installer on the official golang website.

MongoDB Setup

For this project, I assume you already have set up a MongoDB cluster (or a local MongoDB installation) and have the connection URI. If not you can refer to these links for an installation guide: MongoDB cluster or MongoDB local

Project Setup

The first thing we need to do is set up the project by creating a folder for the project. inside the folder, we will initialize the project with go mod and install the packages we are going to use. Run the following commands to set up the project:

go mod init
go get -u github.com/gofiber/fiber/v2
go get go.mongodb.org/mongo-driver/mongo
go get github.com/joho/godotenv
Enter fullscreen mode Exit fullscreen mode

godotenv will allow us to pull in environment variables from a .env file. Create a .env file in the root of the project and add the following:

MONGO_URI=Your_MongoDB_URI_comes_here
DB=Your_DB_name_comes_here
PORT=3000
APP_ENV=development
Enter fullscreen mode Exit fullscreen mode

Next, let's create a .gitignore file in the root of the project and add the following:

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

.env
Enter fullscreen mode Exit fullscreen mode

Start Building the API

Let's create a main.go file at the root of the project. This will contain a basic server setup with a basic route. Add the following to the file:

package main

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

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

    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello, World 👋!")
    })

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

To start the application, run the following command:

go run main.go
Enter fullscreen mode Exit fullscreen mode

Navigate to localhost:3000 in the browser to view the application.

Configuring and Connecting to the database

Always keep all the configurations for the app in a separate folder. Let’s create a new folder config in the root folder of our application for keeping all the configurations.

Create a new file db.go inside the config folder with the following contents:

package config

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/joho/godotenv"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/mongo/readpref"
)

type MongoInstance struct {
    Client *mongo.Client
    DB     *mongo.Database
}

var MI MongoInstance

func ConnectDB() {
    if os.Getenv("APP_ENV") != "production" {
        err := godotenv.Load()
        if err != nil {
            log.Fatal("Error loading .env file")
        }
    }

    client, err := mongo.NewClient(options.Client().ApplyURI(os.Getenv("MONGO_URI")))
    if err != nil {
        log.Fatal(err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    err = client.Connect(ctx)
    if err != nil {
        log.Fatal(err)
    }

    err = client.Ping(ctx, readpref.Primary())
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Database connected!")

    MI = MongoInstance{
        Client: client,
        DB:     client.Database(os.Getenv("DB")),
    }
}
Enter fullscreen mode Exit fullscreen mode

We are going to use the MI object to query the collection.

Creating the Catchphrase Model

Let’s create a new folder models in the root folder of our application for keeping all the models.

Create a new file Catchphrase.go inside the models folder with the following contents:

package models

import (
    "go.mongodb.org/mongo-driver/bson/primitive"
)

type Catchphrase struct {
    ID           primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"`
    MovieName    string             `json:"movieName,omitempty" bson:"movieName,omitempty"`
    Catchphrase  string             `json:"catchphrase,omitempty" bson:"catchphrase,omitempty"`
    MovieContext string             `json:"movieContext,omitempty" bson:"movieContext,omitempty"`
}
Enter fullscreen mode Exit fullscreen mode

Creating the Catchphrases Controller

Let’s create a new folder controllers in the root folder of our application for keeping all the controllers.

Create a new file catchphraseController.go inside the controllers folder with the following contents:

package controllers

import (
    "context"
    "log"
    "math"
    "strconv"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/mikefmeyer/catchphrase-go-mongodb-rest-api/config"
    "github.com/mikefmeyer/catchphrase-go-mongodb-rest-api/models"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo/options"
)

func GetAllCatchphrases(c *fiber.Ctx) error {
    catchphraseCollection := config.MI.DB.Collection("catchphrases")
    ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)

    var catchphrases []models.Catchphrase

    filter := bson.M{}
    findOptions := options.Find()

    if s := c.Query("s"); s != "" {
        filter = bson.M{
            "$or": []bson.M{
                {
                    "movieName": bson.M{
                        "$regex": primitive.Regex{
                            Pattern: s,
                            Options: "i",
                        },
                    },
                },
                {
                    "catchphrase": bson.M{
                        "$regex": primitive.Regex{
                            Pattern: s,
                            Options: "i",
                        },
                    },
                },
            },
        }
    }

    page, _ := strconv.Atoi(c.Query("page", "1"))
    limitVal, _ := strconv.Atoi(c.Query("limit", "10"))
    var limit int64 = int64(limitVal)

    total, _ := catchphraseCollection.CountDocuments(ctx, filter)

    findOptions.SetSkip((int64(page) - 1) * limit)
    findOptions.SetLimit(limit)

    cursor, err := catchphraseCollection.Find(ctx, filter, findOptions)
    defer cursor.Close(ctx)

    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "success": false,
            "message": "Catchphrases Not found",
            "error":   err,
        })
    }

    for cursor.Next(ctx) {
        var catchphrase models.Catchphrase
        cursor.Decode(&catchphrase)
        catchphrases = append(catchphrases, catchphrase)
    }

    last := math.Ceil(float64(total / limit))
    if last < 1 && total > 0 {
        last = 1
    }

    return c.Status(fiber.StatusOK).JSON(fiber.Map{
        "data":      catchphrases,
        "total":     total,
        "page":      page,
        "last_page": last,
        "limit":     limit,
    })
}

func GetCatchphrase(c *fiber.Ctx) error {
    catchphraseCollection := config.MI.DB.Collection("catchphrases")
    ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)

    var catchphrase models.Catchphrase
    objId, err := primitive.ObjectIDFromHex(c.Params("id"))
    findResult := catchphraseCollection.FindOne(ctx, bson.M{"_id": objId})
    if err := findResult.Err(); err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "success": false,
            "message": "Catchphrase Not found",
            "error":   err,
        })
    }

    err = findResult.Decode(&catchphrase)
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "success": false,
            "message": "Catchphrase Not found",
            "error":   err,
        })
    }

    return c.Status(fiber.StatusOK).JSON(fiber.Map{
        "data":    catchphrase,
        "success": true,
    })
}

func AddCatchphrase(c *fiber.Ctx) error {
    catchphraseCollection := config.MI.DB.Collection("catchphrases")
    ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
    catchphrase := new(models.Catchphrase)

    if err := c.BodyParser(catchphrase); err != nil {
        log.Println(err)
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "message": "Failed to parse body",
            "error":   err,
        })
    }

    result, err := catchphraseCollection.InsertOne(ctx, catchphrase)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "success": false,
            "message": "Catchphrase failed to insert",
            "error":   err,
        })
    }
    return c.Status(fiber.StatusCreated).JSON(fiber.Map{
        "data":    result,
        "success": true,
        "message": "Catchphrase inserted successfully",
    })

}

func UpdateCatchphrase(c *fiber.Ctx) error {
    catchphraseCollection := config.MI.DB.Collection("catchphrases")
    ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
    catchphrase := new(models.Catchphrase)

    if err := c.BodyParser(catchphrase); err != nil {
        log.Println(err)
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "message": "Failed to parse body",
            "error":   err,
        })
    }

    objId, err := primitive.ObjectIDFromHex(c.Params("id"))
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "success": false,
            "message": "Catchphrase not found",
            "error":   err,
        })
    }

    update := bson.M{
        "$set": catchphrase,
    }
    _, err = catchphraseCollection.UpdateOne(ctx, bson.M{"_id": objId}, update)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "success": false,
            "message": "Catchphrase failed to update",
            "error":   err.Error(),
        })
    }
    return c.Status(fiber.StatusCreated).JSON(fiber.Map{
        "success": true,
        "message": "Catchphrase updated successfully",
    })
}

func DeleteCatchphrase(c *fiber.Ctx) error {
    catchphraseCollection := config.MI.DB.Collection("catchphrases")
    ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)

    objId, err := primitive.ObjectIDFromHex(c.Params("id"))
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "success": false,
            "message": "Catchphrase not found",
            "error":   err,
        })
    }
    _, err = catchphraseCollection.DeleteOne(ctx, bson.M{"_id": objId})
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "success": false,
            "message": "Catchphrase failed to delete",
            "error":   err,
        })
    }
    return c.Status(fiber.StatusCreated).JSON(fiber.Map{
        "success": true,
        "message": "Catchphrase deleted successfully",
    })
}
Enter fullscreen mode Exit fullscreen mode

The controller file will contain the logic used to query our database.

Creating the Catchphrases Route

Let’s create a new folder routes in the root folder of our application for keeping all the routes.

Create a new file catchphrases.go inside the routes folder with the following contents:

package routes

import (
    "github.com/gofiber/fiber/v2"
    "github.com/mikefmeyer/catchphrase-go-mongodb-rest-api/controllers" // replace
)

func CatchphrasesRoute(route fiber.Router) {
    route.Get("/", controllers.GetAllCatchphrases)
    route.Get("/:id", controllers.GetCatchphrase)
    route.Post("/", controllers.AddCatchphrase)
    route.Put("/:id", controllers.UpdateCatchphrase)
    route.Delete("/:id", controllers.DeleteCatchphrase)
}
Enter fullscreen mode Exit fullscreen mode

Put it all together

Modify the main.go file as follows:

package main

import (
    "log"
    "os"

    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/joho/godotenv"
    "github.com/mikefmeyer/catchphrase-go-mongodb-rest-api/config"
    "github.com/mikefmeyer/catchphrase-go-mongodb-rest-api/routes"
)

func setupRoutes(app *fiber.App) {
    app.Get("/", func(c *fiber.Ctx) error {
        return c.Status(fiber.StatusOK).JSON(fiber.Map{
            "success":     true,
            "message":     "You are at the root endpoint 😉",
            "github_repo": "<https://github.com/MikeFMeyer/catchphrase-go-mongodb-rest-api>",
        })
    })

    api := app.Group("/api")

    routes.CatchphrasesRoute(api.Group("/catchphrases"))
}

func main() {
    if os.Getenv("APP_ENV") != "production" {
        err := godotenv.Load()
        if err != nil {
            log.Fatal("Error loading .env file")
        }
    }

    app := fiber.New()

    app.Use(cors.New())
    app.Use(logger.New())

    config.ConnectDB()

    setupRoutes(app)

    port := os.Getenv("PORT")
    err := app.Listen(":" + port)

    if err != nil {
        log.Fatal("Error app failed to start")
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

After running the application you should be able to navigate to the following route localhost:3000/api/catchphrases to see the catchphrases in your database.

Hosting on Heroku

Heroku allows you to host your application free of charge but with limited resources. To set up the project use the following webpage from the official Heroku documentation.

Note: You might need to add the following config vars to run the application:

MONGO_URI = <Your mongo uri>

DB = <Your database name>

PORT = 8080

APP_ENV = production
Enter fullscreen mode Exit fullscreen mode

Extra

This is the dataset that I used for this API.

Movie Catchphrases Dataset

Thanks for reading

This is a very basic example of a REST API built with Fiber (Go) and MongoDB. The code can be downloaded from GitHub.

💖 💪 🙅 🚩
devwithmike
Dev Mike

Posted on May 25, 2021

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

Sign up to receive the latest update from our blog.

Related