How to Build a Go Web App with Gin, MongoDB, and AI
Rate this tutorial
Building applications with Go provides many advantages. The language is fast, simple, and lightweight while supporting powerful features like concurrency, strong typing, and a robust standard library. In this tutorial, we’ll use the popular Gin web framework along with MongoDB to build a Go-based web application.
Gin is a minimalist web framework for Golang that provides an easy way to build web servers and APIs. It is fast, lightweight, and modular, making it ideal for building microservices and APIs, but can be easily extended to build full-blown applications.
We'll use Gin to build a web application with three endpoints that connect to a MongoDB database. MongoDB is a popular document-oriented NoSQL database that stores data in JSON-like documents. MongoDB is a great fit for building modern applications.
Rather than building the entire application by hand, we’ll leverage a coding AI assistant by Sourcegraph called Cody to help us build our Go application. Cody is the only AI assistant that knows your entire codebase and can help you write, debug, test, and document your code. We’ll use many of these features as we build our application today.
Before you begin, you’ll need:
- Basic familiarity with Go and MongoDB syntax.
- Sourcegraph Cody installed in your favorite IDE. (For this tutorial, we'll be using VS Code). Get it for free.
Once you meet the prerequisites, you’re ready to build. Let’s go.
We'll start by creating a new Go project for our application. For this example, we’ll name the project mflix, so let’s go ahead and create the project directory and navigate into it:
1 mkdir mflix 2 cd mflix
1 go mod init mflix
Now that we have our Go module created, let’s install the dependencies for our project. We’ll keep it really simple and just install the
gin
and mongodb
libraries.1 go get github.com/gin-gonic/gin 2 go get go.mongodb.org/mongo-driver/mongo
With our dependencies fetched and installed, we’re ready to start building our application.
To start building our application, let’s go ahead and create our entry point into the app by creating a main.go file. Next, while we can set up our application manually, we’ll instead leverage Cody to build out our starting point. In the Cody chat window, we can ask Cody to create a basic Go Gin application.
Cody generated a good starting point for us. It imported the Gin framework, created a
main
function, and instantiated a basic Gin application with a single route that prints the message Hello World
. Good start.1 package main 2 3 import ( 4 "github.com/gin-gonic/gin" 5 ) 6 7 func main() { 8 r := gin.Default() 9 r.GET("/", func(c *gin.Context) { 10 c.JSON(200, gin.H{ 11 "message": "Hello World", 12 }) 13 }) 14 15 r.Run() 16 }
Let’s make sure this code runs. Start up the server by running
go run main.go
in your terminal window inside the mflix directory and then navigate to localhost:8080, which is the default port for a Gin application. Our code works and the result we should see is:We have a great starting point now. Next, let’s add our MongoDB client to our Gin application. We could use Cody again, but for this one, let’s write it ourselves. We’ll update the code to the following:
1 package main 2 3 import ( 4 // Add required Go packages 5 "context" 6 "log" 7 8 "github.com/gin-gonic/gin" 9 10 // Add the MongoDB driver packages 11 "go.mongodb.org/mongo-driver/mongo" 12 "go.mongodb.org/mongo-driver/mongo/options" 13 ) 14 15 // Your MongoDB Atlas Connection String 16 const uri = "YOUR-CONNECTION-STRING-HERE" 17 18 // A global variable that will hold a reference to the MongoDB client 19 var mongoClient *mongo.Client 20 21 22 // The init function will run before our main function to establish a connection to MongoDB. If it cannot connect it will fail and the program will exit. 23 func init() { 24 if err := connect_to_mongodb(); err != nil { 25 log.Fatal("Could not connect to MongoDB") 26 } 27 } 28 29 func main() { 30 r := gin.Default() 31 r.GET("/", func(c *gin.Context) { 32 c.JSON(200, gin.H{ 33 "message": "Hello World", 34 }) 35 }) 36 37 r.Run() 38 } 39 40 // Our implementation logic for connecting to MongoDB 41 func connect_to_mongodb() error { 42 serverAPI := options.ServerAPI(options.ServerAPIVersion1) 43 opts := options.Client().ApplyURI(uri).SetServerAPIOptions(serverAPI) 44 45 client, err := mongo.Connect(context.TODO(), opts) 46 if err != nil { 47 panic(err) 48 } 49 err = client.Ping(context.TODO(), nil) 50 mongoClient = client 51 return err 52 }
Be sure to set your MongoDB Atlas connection string on line 12 in the
const uri
variable. Otherwise, the program will not run. You can get your MongoDB Atlas connection string by navigating to the Atlas dashboard, clicking the “Connect” button on your database cluster, and selecting the driver you are using.If you need more help setting up your MongoDB Atlas cluster and loading in the sample data, check out the “How to Use a Sample Database with MongoDB” guide. The database that we will work with is called
sample_mflix
and the collection in that database we’ll use is called movies
. This dataset contains a list of movies with various information like the plot, genre, year of release, and much more.Now that we have our MongoDB database set up in our Go application, we are ready to start building our additional endpoints. Since we’ll be working out of the sample dataset that contains movie information, we’ll create three endpoints based on working with movie data:
- An endpoint to get a list of all the movies.
- An endpoint to get a single movie based on a provided
id
. - An endpoint to run an aggregation on the movies collection.
We can either do this manually or if you’re new to writing Go applications, you can ask Cody. Let’s ask Cody.
Cody gave us three ready-to-go endpoints.
This endpoint will go into the
sample_mflix
database, and then into the movies
collection, and it’ll retrieve all of the movies.1 // GET /movies - Get all movies 2 func getMovies(c *gin.Context) { 3 // Find movies 4 cursor, err := mongoClient.Database("sample_mflix").Collection("movies").Find(context.TODO(), bson.D{{}}) 5 if err != nil { 6 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 7 return 8 } 9 10 // Map results 11 var movies []bson.M 12 if err = cursor.All(context.TODO(), &movies); err != nil { 13 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 14 return 15 } 16 17 // Return movies 18 c.JSON(http.StatusOK, movies) 19 }
The second endpoint will return a specific movie based on the
id
provided from the movies
collection in the sample_mflix
database.1 // GET /movies/:id - Get movie by ID 2 func getMovieByID(c *gin.Context) { 3 // Get movie ID from URL 4 id := c.Param("id") 5 6 // Find movie by ID 7 var movie bson.M 8 err := mongoClient.Database("sample_mflix").Collection("movies").FindOne(context.TODO(), bson.D{{"_id", id}}).Decode(&movie) 9 if err != nil { 10 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 11 return 12 } 13 14 // Return movie 15 c.JSON(http.StatusOK, movie) 16 }
The third and final endpoint will allow us to run aggregations on the movies collection. Aggregation operations process multiple documents and return computed results. So with this endpoint, the end user could pass in any valid MongoDB aggregation pipeline to run various analyses on the
movies
collection.Note that aggregations are very powerful and in a production environment, you probably wouldn’t want to enable this level of access through HTTP request payloads. But for the sake of the tutorial, we opted to keep it in. As a homework assignment for further learning, try using Cody to limit the number of stages or the types of operations that the end user can perform on this endpoint.
1 // POST /movies/aggregations - Run aggregations on movies 2 func aggregateMovies(c *gin.Context) { 3 // Get aggregation pipeline from request body 4 var pipeline interface{} 5 if err := c.ShouldBindJSON(&pipeline); err != nil { 6 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 7 return 8 } 9 10 // Run aggregations 11 cursor, err := mongoClient.Database("sample_mflix").Collection("movies").Aggregate(context.TODO(), pipeline) 12 if err != nil { 13 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 14 return 15 } 16 17 // Map results 18 var result []bson.M 19 if err = cursor.All(context.TODO(), &result); err != nil { 20 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 21 return 22 } 23 24 // Return result 25 c.JSON(http.StatusOK, result) 26 }
Now that we have our endpoints implemented, let’s add them to our router so that we can call them. Here again, we can use another feature of Cody, called autocomplete, to intelligently give us statement completions so that we don’t have to write all the code ourselves.
Our
main
function should now look like:1 func main() { 2 r := gin.Default() 3 r.GET("/", func(c *gin.Context) { 4 c.JSON(200, gin.H{ 5 "message": "Hello World", 6 }) 7 }) 8 r.GET("/movies", getMovies) 9 r.GET("/movies/:id", getMovieByID) 10 r.POST("/movies/aggregations", aggregateMovies) 11 12 r.Run() 13 }
Now that we have our routes set up, let’s test our application to make sure everything is working well. Restart the server and navigate to localhost:8080/movies. If all goes well, you should see a large list of movies returned in JSON format in your browser window. If you do not see this, check your IDE console to see what errors are shown.
Let’s test the second endpoint. Pick any
id
from the movies collection and navigate to localhost:8080/movies/{id} — so for example, localhost:8080/movies/573a1390f29313caabcd42e8. If everything goes well, you should see that single movie listed. But if you’ve been following this tutorial, you actually won’t see the movie.The issue is that in our
getMovie
function implementation, we are accepting the id
value as a string
, while the data type in our MongoDB database is an ObjectID
. So when we run the FindOne
method and try to match the string value of id
to the ObjectID
value, we don’t get a match.Let’s ask Cody to help us fix this by converting the string input we get to an
ObjectID
.Our updated
getMovieByID
function is as follows:1 func getMovieByID(c *gin.Context) { 2 3 // Get movie ID from URL 4 idStr := c.Param("id") 5 6 // Convert id string to ObjectId 7 id, err := primitive.ObjectIDFromHex(idStr) 8 if err != nil { 9 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 10 return 11 } 12 13 // Find movie by ObjectId 14 var movie bson.M 15 err = mongoClient.Database("sample_mflix").Collection("movies").FindOne(context.TODO(), bson.D{{"_id", id}}).Decode(&movie) 16 if err != nil { 17 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 18 return 19 } 20 21 // Return movie 22 c.JSON(http.StatusOK, movie) 23 }
Depending on your IDE, you may need to add the
primitive
dependency in your import statement. The final import statement looks like:1 import ( 2 "context" 3 "log" 4 "net/http" 5 6 "github.com/gin-gonic/gin" 7 "go.mongodb.org/mongo-driver/bson" 8 "go.mongodb.org/mongo-driver/bson/primitive" 9 "go.mongodb.org/mongo-driver/mongo" 10 "go.mongodb.org/mongo-driver/mongo/options" 11 )
If we examine the new code that Cody provided, we can see that we are now getting the value from our
id
parameter and storing it into a variable named idStr
. We then use the primitive package to try and convert the string to an ObjectID
. If the idStr
is a valid string that can be converted to an ObjectID
, then we are good to go and we use the new id
variable when doing our FindOne
operation. If not, then we get an error message back.Restart your server and now try to get a single movie result by navigating to localhost:8080/movies/{id}.
For our final endpoint, we are allowing the end user to provide an aggregation pipeline that we will execute on the
mflix
collection. The user can provide any aggregation they want. To test this endpoint, we’ll make a POST request to localhost:8080/movies/aggregations. In the body of the request, we’ll include our aggregation pipeline.Let’s run an aggregation to return a count of comedy movies, grouped by year, in descending order. Again, remember aggregations are very powerful and can be abused. You normally would not want to give direct access to the end user to write and run their own aggregations ad hoc within an HTTP request, unless it was for something like an internal tool. Our aggregation pipeline will look like the following:
1 [ 2 {"$match": {"genres": "Comedy"}}, 3 {"$group": { 4 "_id": "$year", 5 "count": {"$sum": 1} 6 }}, 7 {"$sort": {"count": -1}} 8 ]
Running this aggregation, we’ll get a result set that looks like this:
1 [ 2 { 3 "_id": 2014, 4 "count": 287 5 }, 6 { 7 "_id": 2013, 8 "count": 286 9 }, 10 { 11 "_id": 2009, 12 "count": 268 13 }, 14 { 15 "_id": 2011, 16 "count": 263 17 }, 18 { 19 "_id": 2006, 20 "count": 260 21 }, 22 ... 23 ]
It seems 2014 was a big year for comedy. If you are not familiar with how aggregations work, you can check out the following resources:
Additionally, you can ask Cody for a specific explanation about how our
aggregateMovies
function works to help you further understand how the code is implemented using the Cody /explain
command.We wrote a Go web server using Gin, MongoDB, and Cody today. While the application may not be the most complex piece of code, we learned how to:
- Build routes and endpoints using the Gin web framework.
- Implement MongoDB in our Gin application.
- Make MongoDB queries to retrieve data.
- Execute MongoDB aggregations.
- Leverage Cody to help us write, debug, and explain code.
The final documented output of all the code we’ve written in this post is below for your reference:
1 // Declare the entry point into our application 2 package main 3 4 // Add our dependencies from the standard library, Gin, and MongoDB 5 import ( 6 "context" 7 "fmt" 8 "log" 9 "net/http" 10 11 "github.com/gin-gonic/gin" 12 "go.mongodb.org/mongo-driver/bson" 13 "go.mongodb.org/mongo-driver/bson/primitive" 14 "go.mongodb.org/mongo-driver/mongo" 15 "go.mongodb.org/mongo-driver/mongo/options" 16 ) 17 18 // Define your MongoDB connection string 19 const uri = "{YOUR-CONNECTION-STRING-HERE}" 20 21 // Create a global variable to hold our MongoDB connection 22 var mongoClient *mongo.Client 23 24 // This function runs before we call our main function and connects to our MongoDB database. If it cannot connect, the application stops. 25 func init() { 26 if err := connect_to_mongodb(); err != nil { 27 log.Fatal("Could not connect to MongoDB") 28 } 29 } 30 31 32 // Our entry point into our application 33 func main() { 34 // The simplest way to start a Gin application using the frameworks defaults 35 r := gin.Default() 36 37 // Our route definitions 38 r.GET("/", func(c *gin.Context) { 39 c.JSON(200, gin.H{ 40 "message": "Hello World", 41 }) 42 }) 43 r.GET("/movies", getMovies) 44 r.GET("/movies/:id", getMovieByID) 45 r.POST("/movies/aggregations", aggregateMovies) 46 47 // The Run() method starts our Gin server 48 r.Run() 49 } 50 51 // Implemention of the /movies route that returns all of the movies from our movies collection. 52 func getMovies(c *gin.Context) { 53 // Find movies 54 cursor, err := mongoClient.Database("sample_mflix").Collection("movies").Find(context.TODO(), bson.D{{}}) 55 if err != nil { 56 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 57 return 58 } 59 60 // Map results 61 var movies []bson.M 62 if err = cursor.All(context.TODO(), &movies); err != nil { 63 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 64 return 65 } 66 67 // Return movies 68 c.JSON(http.StatusOK, movies) 69 } 70 71 72 // The implementation of our /movies/{id} endpoint that returns a single movie based on the provided ID 73 func getMovieByID(c *gin.Context) { 74 75 // Get movie ID from URL 76 idStr := c.Param("id") 77 78 // Convert id string to ObjectId 79 id, err := primitive.ObjectIDFromHex(idStr) 80 if err != nil { 81 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 82 return 83 } 84 85 // Find movie by ObjectId 86 var movie bson.M 87 err = mongoClient.Database("sample_mflix").Collection("movies").FindOne(context.TODO(), bson.D{{"_id", id}}).Decode(&movie) 88 if err != nil { 89 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 90 return 91 } 92 93 // Return movie 94 c.JSON(http.StatusOK, movie) 95 } 96 97 // The implementation of our /movies/aggregations endpoint that allows a user to pass in an aggregation to run our the movies collection. 98 func aggregateMovies(c *gin.Context) { 99 // Get aggregation pipeline from request body 100 var pipeline interface{} 101 if err := c.ShouldBindJSON(&pipeline); err != nil { 102 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 103 return 104 } 105 106 // Run aggregations 107 cursor, err := mongoClient.Database("sample_mflix").Collection("movies").Aggregate(context.TODO(), pipeline) 108 if err != nil { 109 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 110 return 111 } 112 113 // Map results 114 var result []bson.M 115 if err = cursor.All(context.TODO(), &result); err != nil { 116 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 117 return 118 } 119 120 // Return result 121 c.JSON(http.StatusOK, result) 122 } 123 124 125 // Our implementation code to connect to MongoDB at startup 126 func connect_to_mongodb() error { 127 serverAPI := options.ServerAPI(options.ServerAPIVersion1) 128 opts := options.Client().ApplyURI(uri).SetServerAPIOptions(serverAPI) 129 130 client, err := mongo.Connect(context.TODO(), opts) 131 if err != nil { 132 panic(err) 133 } 134 err = client.Ping(context.TODO(), nil) 135 mongoClient = client 136 return err 137 }
Go is an amazing programming language and Gin is a very powerful framework for building web applications. Combined with MongoDB and the native MongoDB driver, and with a little help from Cody, we were able to build this app in no time at all.
Cody is the only AI assistant that knows your entire codebase. In this tutorial, we barely scratched the surface of what’s possible. Beyond autocomplete and the commands we showed today, Cody can identify code smells, document your code, create unit tests, and support the creation of custom commands to extend it to whatever use case you have. Give Cody a try for free at cody.dev.
The entire code for our application is above, so there is no GitHub repo for this simple application. Happy coding.