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
moviescollection
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.
Set Up Your Project
Create the Application Structure
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:
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) } }
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.
Add route handlers
Add the following route handlers after your connectToMongoDB function:
// 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) }
Register routes
Update your main function to register the new routes:
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}} ]'
[ { "_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 and authorization
Add indexes to improve query performance
Additional Resources
To learn more about the concepts from this tutorial, see the following resources: