Tutorial: Build a Web Application with the Go driver and Gin

For the complete MongoDB documentation index, see www.mongodb.com/docs/llms.txt

Tutorial: Build a Web Application with the Go driver and Gin

Overview

This tutorial shows you how to build a web application by using the MongoDB Go Driver and the Gin web framework.

Gin is a fast, lightweight web framework for Go. Its minimalist and modular design makes it ideal for building web servers, APIs, and microservices.

In this tutorial, you can learn how to create a web application with the following endpoints that connect to a MongoDB database:

  • An endpoint to retrieve all movies from a collection

  • An endpoint to retrieve a single movie by ID

  • An endpoint to run aggregation operations on the movies collection

Sample Data

The tutorial uses the movies collection in the sample_mflix database from the Atlas sample datasets. To learn how to create a free MongoDB Atlas cluster and load the sample datasets, see the Get Started with Atlas guide.

Prerequisites

Before you begin this tutorial, ensure that you have the following:

  • Go installed on your development machine

  • A MongoDB deployment. You can use a free MongoDB Atlas cluster for this tutorial. For more information, see the Get Started with Atlas guide.

  • A tool for testing your API, such as curl or Postman.

Set Up Your Project

  1. Create and initialize your project

Create a new directory for your project and navigate into it:

bash mkdir mflix cd mflix

Initialize a new Go module to manage your project dependencies:

bash go mod init mflix

  1. Add the Gin framework and the MongoDB Go Driver as dependencies

Install the required dependencies for your project:

bash go get github.com/gin-gonic/gin go get go.mongodb.org/mongo-driver/v2/mongo

Create the Application Structure

  1. Create the main application file

Create a main.go file in your project directory and add the following code to set up a basic Gin application:

```go package main

import ( "log"

"github.com/gin-gonic/gin"

)

func main() { r := gin.Default() r.GET("/", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "Hello World", }) })

if err := r.Run(); err != nil {
    log.Fatal("Failed to start server:", err)
}

}

```

  1. Run the application

Run the following code to test that your basic application is working:

bash go run main.go

Navigate to http://localhost:8080 in your web browser to see a JSON response with the message “Hello World”.

Connect to MongoDB

Update your main.go file to connect to MongoDB. Replace the existing code with the following code to connect to your MongoDB deployment:

package main

import (
    "context"
    "log"

    "github.com/gin-gonic/gin"
    "go.mongodb.org/mongo-driver/v2/mongo"
    "go.mongodb.org/mongo-driver/v2/mongo/options"
)

// Replace with your MongoDB connection string
const uri = "<connection-string>"

func main() {
    // Connects to MongoDB
    serverAPI := options.ServerAPI(options.ServerAPIVersion1)
    opts := options.Client().ApplyURI(uri).SetServerAPIOptions(serverAPI)

    client, err := mongo.Connect(opts)
    if err != nil {
        log.Fatal("Could not connect to MongoDB:", err)
    }

    // Ensures the client disconnects when main exits
    defer func() {
        if err := client.Disconnect(context.TODO()); err != nil {
            log.Fatal("Error disconnecting from MongoDB:", err)
        }
    }()

    // Pings the database to verify connection
    if err := client.Ping(context.TODO(), nil); err != nil {
        log.Fatal("Could not ping MongoDB:", err)
    }

    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "Hello World",
        })
    })

    if err := r.Run(); err != nil {
        log.Fatal("Failed to start server:", err)
    }
}

Replace the <connection-string> placeholder with your actual MongoDB connection string. To learn more about how to obtain your connection string, see the Connect to a Cluster guide.

Implement the API Endpoints

Now, you can add three endpoints to interact with the movie data. Update your main.go file to include the necessary imports and endpoint implementations.

  1. Update imports

Add the required imports at the top of your file:

```go import ( "context" "log" "net/http"

"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"

)

```

  1. Add route handlers

Add the following route handlers after your connectToMongoDB function:

```go // GET /movies - Retrieves all movies func getMovies(c gin.Context, client mongo.Client) { // Find all movies in the collection cursor, err := client.Database("sample_mflix").Collection("movies").Find(c.Request.Context(), bson.D{}) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer cursor.Close(c.Request.Context())

// Decodes all results
var movies []bson.D
if err = cursor.All(c.Request.Context(), &movies); err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
}

// Returns the movies
c.JSON(http.StatusOK, movies)

}

// GET /movies/:id - Retrieves a movie by ID func getMovieByID(c gin.Context, client mongo.Client) { // Gets the movie ID from the URL parameter as a string idStr := c.Param("id")

// Converts string ID to MongoDB ObjectID
id, err := bson.ObjectIDFromHex(idStr)
if err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid movie ID format"})
    return
}

// Finds the movie by ObjectID
var movie bson.D
err = client.Database("sample_mflix").Collection("movies").FindOne(c.Request.Context(), bson.D{{"_id", id}}).Decode(&movie)
if err != nil {
    if err == mongo.ErrNoDocuments {
        c.JSON(http.StatusNotFound, gin.H{"error": "Movie not found"})
        return
    }
    c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
}

// Returns the movie
c.JSON(http.StatusOK, movie)

}

// POST /movies/aggregations - Runs aggregation pipeline func aggregateMovies(c gin.Context, client mongo.Client) { // Gets aggregation pipeline from request body var pipeline interface{} if err := c.ShouldBindJSON(&pipeline); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid aggregation pipeline"}) return }

// Executes the aggregation pipeline
cursor, err := client.Database("sample_mflix").Collection("movies").Aggregate(c.Request.Context(), pipeline)
if err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
}
defer cursor.Close(c.Request.Context())

// Decodes the results
var result []bson.D
if err = cursor.All(c.Request.Context(), &result); err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
}

// Returns the aggregation result
c.JSON(http.StatusOK, result)

}

```

  1. Register routes

Update your main function to register the new routes:

```go func main() { // Connects to MongoDB serverAPI := options.ServerAPI(options.ServerAPIVersion1) opts := options.Client().ApplyURI(uri).SetServerAPIOptions(serverAPI)

client, err := mongo.Connect(opts)
if err != nil {
    log.Fatal("Could not connect to MongoDB:", err)
}

// Ensures the client disconnects when main exits
defer func() {
    if err := client.Disconnect(context.TODO()); err != nil {
        log.Fatal("Error disconnecting from MongoDB:", err)
    }
}()

// Pings the database to verify connection
if err := client.Ping(context.TODO(), nil); err != nil {
    log.Fatal("Could not ping MongoDB:", err)
}

r := gin.Default()

r.GET("/", func(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "Hello World",
    })
})

// Registers movie endpoints
r.GET("/movies", func(c *gin.Context) {
    getMovies(c, client)
})
r.GET("/movies/:id", func(c *gin.Context) {
    getMovieByID(c, client)
})
r.POST("/movies/aggregations", func(c *gin.Context) {
    aggregateMovies(c, client)
})

if err := r.Run(); err != nil {
    log.Fatal("Failed to start server:", err)
}

}

```

Test Your Application

You can test the endpoints you created to ensure they work as expected.

Test Get All Movies

Restart your server and navigate to http://localhost:8080/movies in your web browser. You should see a JSON response containing an array of movies from the sample dataset.

Test Get Movie by ID

To test the single movie endpoint, copy an _id value from the movies response and navigate to http://localhost:8080/movies/{id}.

For example, if you navigate to http://localhost:8080/movies/573a1390f29313caabcd42e8, the response in the browser is similar to the following:

{
   "_id": "573a1390f29313caabcd42e8",
   "awards": {
      "wins": 1,
      "nominations": 0,
      "text": "1 win."
   },
   "cast": [
      "A.C. Abadie",
      "Gilbert M. 'Broncho Billy' Anderson",
      "George Barnes",
      "Justus D. Barnes"
   ],
   "countries": [
      "USA"
   ],
   "directors": [
      "Edwin S. Porter"
   ],
   "fullplot": "Among the earliest existing films in American cinema - notable as the first film that presented a narrative story to tell - it depicts a group of cowboy outlaws who hold up a train and rob the passengers. They are then pursued by a Sheriff's posse. Several scenes have color included - all hand tinted.",
   "genres": [
      "Short",
      "Western"
   ],"
   ....
}

Test Aggregation Endpoint

To test the aggregation endpoint, you need to make a POST request. You can use a tool like curl or Postman to send a request with an aggregation pipeline in the request body.

The following example aggregation counts comedy movies by year:

[
    {"$match": {"genres": "Comedy"}},
    {"$group": {
        "_id": "$year",
        "count": {"$sum": 1}
    }},
    {"$sort": {"count": -1}}
]

Run the following code in your terminal to test this endpoint with curl:

curl -X POST http://localhost:8080/movies/aggregations \
     -H "Content-Type: application/json" \
     -d '[
         {"$match": {"genres": "Comedy"}},
         {"$group": {"_id": "$year", "count": {"$sum": 1}}},
         {"$sort": {"count": -1}}
     ]'

Output:

 [
    {
        "_id": 2014,
        "count": 287
    },
    {
        "_id": 2013,
        "count": 286
    },
    {
        "_id": 2009,
        "count": 268
    },
    {
        "_id": 2011,
        "count": 263
    },
    {
        "_id": 2006,
        "count": 260
    },
    ...
  ]

Warning:

The aggregation endpoint in this tutorial allows users to run any aggregation pipeline. In a production environment, you should limit the types of operations and validate input to prevent potential security issues or performance problems.

Complete Code

The following file shows the complete code for your web application:

package main

import (
    "context"
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
    "go.mongodb.org/mongo-driver/v2/bson"
    "go.mongodb.org/mongo-driver/v2/mongo"
    "go.mongodb.org/mongo-driver/v2/mongo/options"
)

// Replace with your MongoDB connection string
const uri = "YOUR-CONNECTION-STRING-HERE"

func main() {
    // Connects to MongoDB
    serverAPI := options.ServerAPI(options.ServerAPIVersion1)
    opts := options.Client().ApplyURI(uri).SetServerAPIOptions(serverAPI)

    client, err := mongo.Connect(opts)
    if err != nil {
        log.Fatal("Could not connect to MongoDB:", err)
    }

    // Ensures the client disconnects when main exits
    defer func() {
        if err := client.Disconnect(context.TODO()); err != nil {
            log.Fatal("Error disconnecting from MongoDB:", err)
        }
    }()

    // Pings the database to verify connection
    if err := client.Ping(context.TODO(), nil); err != nil {
        log.Fatal("Could not ping MongoDB:", err)
    }

    r := gin.Default()

    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "Hello World",
        })
    })

    // Registers movie endpoints
    r.GET("/movies", func(c *gin.Context) {
        getMovies(c, client)
    })
    r.GET("/movies/:id", func(c *gin.Context) {
        getMovieByID(c, client)
    })
    r.POST("/movies/aggregations", func(c *gin.Context) {
        aggregateMovies(c, client)
    })

    if err := r.Run(); err != nil {
        log.Fatal("Failed to start server:", err)
    }
}

// GET /movies - Retrieves all movies
func getMovies(c *gin.Context, client *mongo.Client) {
    // Find all movies in the collection
    cursor, err := client.Database("sample_mflix").Collection("movies").Find(c.Request.Context(), bson.D{})
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    defer cursor.Close(c.Request.Context())

    // Decodes all results
    var movies []bson.D
    if err = cursor.All(c.Request.Context(), &movies); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    // Returns the movies
    c.JSON(http.StatusOK, movies)
}

// GET /movies/:id - Retrieves a movie by ID
func getMovieByID(c *gin.Context, client *mongo.Client) {
    // Gets the movie ID from the URL parameter as a string
    idStr := c.Param("id")

    // Converts string ID to MongoDB ObjectID
    id, err := bson.ObjectIDFromHex(idStr)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid movie ID format"})
        return
    }

    // Finds the movie by ObjectID
    var movie bson.D
    err = client.Database("sample_mflix").Collection("movies").FindOne(c.Request.Context(), bson.D{{"_id", id}}).Decode(&movie)
    if err != nil {
        if err == mongo.ErrNoDocuments {
            c.JSON(http.StatusNotFound, gin.H{"error": "Movie not found"})
            return
        }
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    // Returns the movie
    c.JSON(http.StatusOK, movie)
}

// POST /movies/aggregations - Runs aggregation pipeline
func aggregateMovies(c *gin.Context, client *mongo.Client) {
    // Gets aggregation pipeline from request body
    var pipeline interface{}
    if err := c.ShouldBindJSON(&pipeline); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid aggregation pipeline"})
        return
    }

    // Executes the aggregation pipeline
    cursor, err := client.Database("sample_mflix").Collection("movies").Aggregate(c.Request.Context(), pipeline)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    defer cursor.Close(c.Request.Context())

    // Decodes the results
    var result []bson.D
    if err = cursor.All(c.Request.Context(), &result); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    // Returns the aggregation result
    c.JSON(http.StatusOK, result)
}

Summary

You now have a basic application that demonstrates the following:

  • Sets up a Go project with the Gin web framework and the Go driver.

  • Connects to a MongoDB database by using the Go driver.

  • Implements REST API endpoints that perform MongoDB operations to find documents, find a single document, and run aggregation pipelines.

  • Handles common scenarios such as converting string IDs to ObjectIDs.

  • Implements proper error handling for database operations.

Next Steps

To further develop your application, consider the following next steps:

  • Add input validation and request limiting for the aggregation endpoint

  • Implement [authentication](https://www.mongodb.com/docs/drivers/go/current/security/authentication.md#std-label-golang-enterprise-authentication-mechanisms and authorization

  • Add [indexes](https://www.mongodb.com/docs/drivers/go/current/indexes.md#std-label-golang-indexes to improve query performance

Additional Resources

To learn more about the concepts from this tutorial, see the following resources:

  • [Getting Started with Go](https://www.mongodb.com/docs/drivers/go/current/get-started.md#std-label-go-get-started

  • [Find Documents](https://www.mongodb.com/docs/drivers/go/current/crud/query/retrieve.md#std-label-golang-retrieve

  • [Aggregation](https://www.mongodb.com/docs/drivers/go/current/aggregation.md#std-label-golang-aggregation

  • Gin Web Framework