DEV Community

loading...
Cover image for Build an API endpoint with GOlang , Gin & mongoDB

Build an API endpoint with GOlang , Gin & mongoDB

joojodontoh profile image JOOJO DONTOH Updated on ・6 min read

Note worthy📝

  • This article assumes that you already have golang installed on your computer. If you haven't, kindly do so here
  • This article assumes you have an idea of http request methods.

Lets get started!!

  • Since we will be using MongoDB, we will first start by setting that up. Follow the steps below to setup your MongoDB free cluster.
  1. Head over to MongoDB and register

  2. Set your account up by filling in the details and select GO as your prefered language. (https://www.mongodb.com/cloud/atlas/register)

    register

  3. Since this is not a production application, we'll be using the free cluster. select free cluster

  4. Now select the M0 Sandbox cluster tier and leave the rest of the items as default
    select cluster tier

  5. Once everything is done loading click the connect to set up your connection. Select "Allow access from Anywhere" (in a production application you may want to specify the IP address) and fill in the description. Also create a database user by entering your prefered details.
    Alt Text

  6. Now choose "Connect your application", select the driver and version, and then copy the string (store it in a safe place). Remember to substitute the details in the connection string with the database user you created earlier.

Creating the API

Running the application

$ go build
$ ./new
Enter fullscreen mode Exit fullscreen mode

go build compiles the application. ./new is used to run the compiled file. This file will be located in your root folder and may have another name. I simply named mine new.

  • Create a folder and name it whatever you please. I'm gonna name mine DEVTO. This is going to be your root folder.

  • My prefered IDE is VS code. open the terminal in VScode and run go mod init. This will automatically create your go.mod file (a file that stores all the required packages for your app) and save it in your root folder.

  • Create a .env file (a file for storing your environmental variables) and fill it with the following.

PORT=5000
MONGODB_URL={{insert the db link here}}
Enter fullscreen mode Exit fullscreen mode
  • Create a folder (package) named "database" this folder will contain a file which manages your database connection and opens your collections. create the file and name "databaseConnection.go" and fill it with the following code.
package database

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

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

//DBinstance func
func DBinstance() *mongo.Client {
    err := godotenv.Load(".env")

    if err != nil {
        log.Fatal("Error loading .env file")
    }

    MongoDb := os.Getenv("MONGODB_URL")

    client, err := mongo.NewClient(options.Client().ApplyURI(MongoDb))
    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)
    }
    fmt.Println("Connected to MongoDB!")

    return client
}

//Client Database instance
var Client *mongo.Client = DBinstance()

//OpenCollection is a  function makes a connection with a collection in the database
func OpenCollection(client *mongo.Client, collectionName string) *mongo.Collection {

    var collection *mongo.Collection = client.Database("cluster0").Collection(collectionName)

    return collection
}

Enter fullscreen mode Exit fullscreen mode
  • Create a Main.go file in your root folder and add the package name and needed packages for this application
package main

import (
    "context"
    "fmt"
    "math"
    "net/http"
    "os"
    "time"

    "dev.to/new/database"
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
    _ "github.com/heroku/x/hmetrics/onload"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
)
Enter fullscreen mode Exit fullscreen mode
  • Let's assume we need an endpoint that can be used to create food items for a restaurant. A food item should have the following attributes:
  • Name ✅ ,
  • price 💰 (because food is not free),
  • An image 🖼 ,
  • An ID number to uniquely identify it, and
  • Time stamps to indicated creation and update time ⏲ .

Now that we know our attributes, we are going to create a struct to contain all the attributes of our food object. A struct contains the name and type of the attributes. It may also contain the json names and validator instructions for each attribute.

//this object/struct determines what a food object should look like.
type Food struct {
    ID         primitive.ObjectID `bson:"_id"`
    Name       *string            `json:"name" validate:"required,min=2,max=100"`
    Price      *float64           `json:"price" validate:"required"`
    Food_image *string            `json:"food_image" validate:"required"`
    Created_at time.Time          `json:"created_at"`
    Updated_at time.Time          `json:"updated_at"`
    Food_id    string             `json:"food_id"`
}
Enter fullscreen mode Exit fullscreen mode
  • Now we need to create a new validator object which will be used to corroborate request payloads

// create a validator object
var validate = validator.New()
Enter fullscreen mode Exit fullscreen mode
  • We will also need to add two functions that come together to round the price of the food to 2 decimal places. This is to keep a consistent record of pricing in the DB.
//this function rounds the price value down to 2 decimal places
func ToFixed(num float64, precision int) float64 {
    output := math.Pow(10, float64(precision))
    return float64(Round(num*output)) / output
}
func Round(num float64) int {
    return int(num + math.Copysign(0.5, num))
}
Enter fullscreen mode Exit fullscreen mode
  • Since we already have our mongoDB connection string and our database connection package, we can now both connect to the DB and open the food collection.
//connect to to the database and open a food collection
var foodCollection *mongo.Collection = database.OpenCollection(database.Client, "food")
Enter fullscreen mode Exit fullscreen mode
  • Now we are at the main function which is the entry point to your application. In this function we are going to first retrieve the PORT number from the .env file we created earlier, while error-checking it for empty values so that our app doesn't crash if in any case it's not found.

func main() {

    port := os.Getenv("PORT")
    if port == "" {
        port = "8000"
    }
Enter fullscreen mode Exit fullscreen mode
  • We are now about to have our first encounter with the gin framework. we will create a new router object from gin and use their logger function. This function prints API requests to the console when they are made. All this is happening in the main function.

    router := gin.New()
    router.Use(gin.Logger())
Enter fullscreen mode Exit fullscreen mode
  • Everything seems ready to create our endpoint in our main fucntion so that's exactly what we're gonna do. SInce this is a create endpoint, we'll be using the POST method in the router object we created earlier. Our endpoint is going to be named /foods-create.

    // this is the create food endpoint
    router.POST("/foods-create", func(c *gin.Context) {
Enter fullscreen mode Exit fullscreen mode
  • Now declare the context for the context for API calls to your application.
        //this is used to determine how long the API call should last
        var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
Enter fullscreen mode Exit fullscreen mode
  • Also Declare a food variable of type Food(the struct we wrote earlier)

        //declare a variable of type food
        var food Food
Enter fullscreen mode Exit fullscreen mode
  • We will use the food variable we just declared to contain the food object from any request payload by binding the JSON object in the request payload.

        //bind the object that comes in with the declared varaible. thrrow an error if one occurs
        if err := c.BindJSON(&food); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
Enter fullscreen mode Exit fullscreen mode
  • We will now use the validation object we created earlier to validate the payload based on the Food struct we created.

        // use the validation packge to verify that all items coming in meet the requirements of the struct
        validationErr := validate.Struct(food)
        if validationErr != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": validationErr.Error()})
            return
        }
Enter fullscreen mode Exit fullscreen mode
  • Now we need to set the timestamp for creation.

        // assign the time stamps upon creation
        food.Created_at, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
        food.Updated_at, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
Enter fullscreen mode Exit fullscreen mode
  • A new ID must be generated to uniquely identify the food item while rounding the value of the price attribute.

        //generate new ID for the object to be created
        food.ID = primitive.NewObjectID()

        // assign the the auto generated ID to the primary key attribute
        food.Food_id = food.ID.Hex()
        var num = ToFixed(*food.Price, 2)
        food.Price = &num
Enter fullscreen mode Exit fullscreen mode
  • Our endpoint has now reached the end of its process for creating the food item. It's time for us to insert it into the DB and return the created ID number to the frontend.

        //insert the newly created object into mongodb
        result, insertErr := foodCollection.InsertOne(ctx, food)
        if insertErr != nil {
            msg := fmt.Sprintf("Food item was not created")
            c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
            return
        }
        defer cancel()

        //return the id of the created object to the frontend
        c.JSON(http.StatusOK, result)

    })
Enter fullscreen mode Exit fullscreen mode
  • Configure your application to listen for API requests and close your main function. Our endpoint is good to GO!

    //this runs the server and allows it to listen to requests.
    router.Run(":" + port)
}

Enter fullscreen mode Exit fullscreen mode
  • Run you application as stated earlier and test it in postman like so: Postman

Check the repository for this tutorial out!

Discussion (3)

pic
Editor guide
Collapse
singhayushh profile image
Ayush Singh

I have a question here.

Looking at the DBInstance and OpenCollection functions, it seems the app is dialing up connection to the database for every api call, or at every instance a collection is being used. So isn't there a roundabout way to have the database connection on as long as the server is running so that you don't have to dial up and end connections to the db for every call.

Collapse
joojodontoh profile image
JOOJO DONTOH Author

Hi Ayush,
Thank you for you question. As far as I know, you can use the "gopkg.in/mgo.v2" package to create connection sessions which can be sent in the context of your router/http handler when API calls are made. However the common occurrence is that most people close (db.cose/ defer cancel) the db session after it has been used to free up resources that may be put back in the pool or collected, depending on the case. This also prevents memory leaks. You are absolutely right about dialing up a connection to the database for every api call. I can change this by creating the database client once the server starts running in the main.go file, and then send the client within context to various routes that need it. This should keep the connection open during server life for I/O actions on collections

Collapse
singhayushh profile image
Ayush Singh

Alright, it's a lot clear now.
Thank you for clearing my doubt!