RESTful API with gorilla/mux and MongoDB

In the previous chapters, we explored all the possible ways of building a RESTful API. We used basic HTTP routers, as well as many other web frameworks. However, to keep it simple, we can use gorilla/mux with mongo-driver for the MongoDB driver. In this section, we will build an end-to-end movie API while integrating the database and HTTP router. In the previous section, we learned how to create a new MongoDB document and retrieve it using mongo-driver. By consolidating our knowledge of HTTP routers and databases, we can create a movie API.

Let's create the plan so that we can create the API:

  1. Prepare structs to hold movie information and the database connection.
  2. Create a server for hosting the API.
  3. Prepare the routes for the API endpoints.
  4. Implement handlers for the routes.

We have to follow these steps to achieve our goal:

  1. Create a directory to hold our project:
mkdir $GOPATH/src/github.com/git-user/chapter5/movieAPI
  1. Add a main.go file in the project:
touch $GOPATH/src/github.com/git-user/chapter5/movieAPI/main.go
Please install the mongo-driver package using the dep tool, just like we did in the previous section.
  1. Let's take a look at the structs we need to create; that is, DB, Movie, and BoxOffice. Movie and BoxOffice hold the movie information. The DB struct holds a collection in a MongoDB database that can be passed across multiple functions. The code for this is as follows:
type DB struct {
collection *mongo.Collection
}

type Movie struct {
ID interface{} `json:"id" bson:"_id,omitempty"`
Name string `json:"name" bson:"name"`
Year string `json:"year" bson:"year"`
Directors []string `json:"directors" bson:"directors"`
Writers []string `json:"writers" bson:"writers"`
BoxOffice BoxOffice `json:"boxOffice" bson:"boxOffice"`
}

type BoxOffice struct {
Budget uint64 `json:"budget" bson:"budget"`
Gross uint64 `json:"gross" bson:"gross"`
}
  1. We need a few important packages in order to implement our API. These are gorilla/mux, mongo-driver, and a few other helper packages. Let's look at how to import these packages:
  ...
"go.mongodb.org/mongo-driver/bson/primitive"

"go.mongodb.org/mongo-driver/bson"

"github.com/gorilla/mux"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"

We need the primitive package to generate an ObjectID from a string, the bson package to create query filters, and the mongo/options package to create a MongoDB client.

  1. Let's create the main function, which is where we create a MongoDB client. The client is created by passing options to the Connect method. Once we are connected to MongoDB, which is running locally on port 27017, we can access the collection using the Database.Collection method. We can delay cleaning up the connection using the defer  keyword:
func main() {
clientOptions :=
options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
panic(err)
}
defer client.Disconnect(context.TODO())

collection := client.Database("appDB").Collection("movies")
db := &DB{collection: collection}
...
}
The defer keyword is special in a Go program. It defers a function call so that it's executed right before the enclosing outer function returns. It is commonly used for I/O connection cleanup.

In our case, the enclosing function is the main, and the deferred function is client.Disconnect. So, when main returns/terminates, the defer statement closes the MongoDB connection properly.
  1. Next, we create a few HTTP routes for the GET and POST operations on a movie. Let's call them GetMovie and PostMovierespectively. The code looks like this:
  r := mux.NewRouter()
r.HandleFunc("/v1/movies/{id:[a-zA-Z0-9]*}",
db.GetMovie).Methods("GET")
r.HandleFunc("/v1/movies", db.PostMovie).Methods("POST")
  1. Now, we can start a server using the http.Server method, as shown in the following code:
  srv := &http.Server{
Handler: r,
Addr: "127.0.0.1:8000",
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
  1. Now comes the actual implementation of the handlers. GetMovie, like any other mux handler, takes response and request objects. It receives an ObjectId (hex string) of the movie from the path parameters and queries a matching document from the database. We can use the mux.Vars map to collect path parameters.

We can't simply form a filter query using the raw ID. We have to convert the hex string that was passed into the ObjectID using the primitive.ObjectIDFromHex method from the mongo-driver/bson/primitive package. We should use this ObjectID in a filter query.

Then, we run a query using the collection.FindOne method. The result can then be decoded into a Movie struct literal and returned as a JSON response. Take a look at the following code for the GetMovie function handler:

// GetMovie fetches a movie with a given ID
func (db *DB) GetMovie(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var movie Movie
objectID, _ := primitive.ObjectIDFromHex(vars["id"])
filter := bson.M{"_id": objectID}
err := db.collection.FindOne(context.TODO(),
filter).Decode(&movie)

if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
} else {
w.Header().Set("Content-Type", "application/json")
response, _ := json.Marshal(movie)
w.WriteHeader(http.StatusOK)
w.Write(response)
}
}
  1. PostMovie has the exact same function signature as the GET handler function. Instead of reading from the path parameters, it reads information from the request body in JSON and un-marshalls it into the Movie struct. Then, we use the collection.InsertOne method and perform a database insert operation. The result of the JSON is sent back as an HTTP response. The code for the PostMovie handler function looks like this:
// PostMovie adds a new movie to our MongoDB collection
func (db *DB) PostMovie(w http.ResponseWriter, r *http.Request) {
var movie Movie
postBody, _ := ioutil.ReadAll(r.Body)
json.Unmarshal(postBody, &movie)

result, err := db.collection.InsertOne(context.TODO(), movie)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
} else {
w.Header().Set("Content-Type", "application/json")
response, _ := json.Marshal(result)
w.WriteHeader(http.StatusOK)
w.Write(response)
}
}
  1. Now, let's run the program:
go run $GOPATH/src/github.com/git-user/chapter5/movieAPI/main.go
  1. Next, we open a Terminal and make a POST API request using curl or Postman to create a new movie:
curl -X POST \
http://localhost:8000/v1/movies \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-d '{ "name" : "The Dark Knight", "year" : "2008", "directors" : [ "Christopher Nolan" ], "writers" : [ "Jonathan Nolan", "Christopher Nolan" ], "boxOffice" : { "budget" : 185000000, "gross" : 533316061 }
}'

This returns the following response:

{"InsertedID":"5cfd6cf0c281945c6cfefaab"}
  1. Our movie has been created successfully. Next, let's retrieve it. Make a GET API request using curl:
curl -X GET http://localhost:8000/v1/movies/5cfd6cf0c281945c6cfefaab

It returns the same data that we got while creating the resource:

{"id":"5cfd6cf0c281945c6cfefaab","name":"The Dark Knight","year":"2008","directors":["Christopher Nolan"],"writers":["Jonathan Nolan","Christopher Nolan"],"boxOffice":{"budget":185000000,"gross":533316061}}
  1. We can easily add PUT (update) and DELETE methods to/from the preceding code. We just need to define two more handlers. First, look at the UpdateMovie handler. It gets the ObjectID as a path parameter in order to update a document in MongoDB, as shown in the following code:
// UpdateMovie modifies the data of given resource
func (db *DB) UpdateMovie(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var movie Movie
putBody, _ := ioutil.ReadAll(r.Body)
json.Unmarshal(putBody, &movie)

objectID, _ := primitive.ObjectIDFromHex(vars["id"])
filter := bson.M{"_id": objectID}
update := bson.M{"$set": &movie}
_, err := db.collection.UpdateOne(context.TODO(), filter, update)
...
}

  1. Next, the handler function is DeleteMovie. It gets the object ID from the path parameters and tries to delete a document with the same ID in the database using the DeleteOne method, like this:
// DeleteMovie removes the data from the db
func (db *DB) DeleteMovie(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
objectID, _ := primitive.ObjectIDFromHex(vars["id"])
filter := bson.M{"_id": objectID}

_, err := db.collection.DeleteOne(context.TODO(), filter)
...
}

In these API operations, we can also simply send the status back to the client with no HTTP body.

For these handlers to be activated by gorilla/mux, we have to register two new HTTP endpoints to the router, like this:

r.HandleFunc("/v1/movies/{id:[a-zA-Z0-9]*}", db.UpdateMovie).Methods("PUT")
r.HandleFunc("/v1/movies/{id:[a-zA-Z0-9]*}", db.DeleteMovie).Methods("DELETE")

The complete code for these additions is available in the chapter5/movieAPI_updated/main.go file. If you run the updated program, you will have a full CRUD-based API with MongoDB as a backend.