Go, Gorilla/mux and mongo demo

iamelesq

elesq

Posted on August 2, 2021

Go, Gorilla/mux and mongo demo

What are we going to do?

  1. Setup some tools for starting a containerised MongoDB and connecting to the shell.
  2. Create a connection to the database with Go
  3. perform some trivial CRUD.
  4. get an MVP knocked up, not paint the Sistine Chapel.

Simple. Let's go.

Docker setup

I'm assuming you have docker installed, if not head over to the docker site, follow the installation instructions for you system. Please forgive anything in the following section that is Mac specific and an oversight to make it OS agnostic. OK so all good, Docker installed. Let's get an image of MongoDB.

docker pull mongo
Enter fullscreen mode Exit fullscreen mode

Now we have an image successfully pulled you'll want to be able to start a container running it. There's couple of things here, you should read up on detached/interactive modes for docker. It's fairly self explanatory and you can control it with cli flags when starting a container -d detached mode, -it interactive mode respectively. Another thing we have to be mindful of is binding a container port to a local port. So to get a container running that is in detached mode and bound locally to the usual mongoDB port of 27017 and with the ubiquitous admin/password combo you can use a command like this

docker run -d --name mongodb -v ~/data:/data/db -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -p 27017:27017 mongo:4.4.3
Enter fullscreen mode Exit fullscreen mode

This is quite a handful to memorise, so what I like to do with stuff like this is take the pain one time, drop that command in a runnable script that I can dump in my scripts folder and assign an alias to it so I can weave magic with a one or two keystroke combo.

To do so, I'm creating a script called startmongo.sh

#!/bin/sh

echo " ┌─┐┌┬┐┌─┐┬─┐┌┬┐        "
echo " └─┐ │ ├─┤├┬┘ │          "
echo " └─┘ ┴ ┴ ┴┴└─ ┴          "
echo " ┌┬┐┌─┐┌┐┌┌─┐┌─┐┌┬┐┌┐  "
echo " ││││ │││││ ┬│ │ ││├┴┐  "
echo " ┴ ┴└─┘┘└┘└─┘└─┘─┴┘└─┘ "
echo " "
echo " Starting a docker container running MongoDB...."


docker run --name postgres -e POSTGRES_DB=vapor_database -e POSTGRES_USER=ed -e POSTGRES_PASSWORD=foolsgold -p 5432:5432 postgres
Enter fullscreen mode Exit fullscreen mode

This gives a jazzy little header and starts my container. Great stuff. Next I want to be able to connect to the shell incase I have any brut force operations that I want to do on the CLI. Again, I'm doing this once, sticking it in an easy script and hiding that behind an alias. The command is docker exec -it mongodb bash to connect interactively to the container called mongoDB with a bash shell.

#!/bin/sh

echo " ┌┬┐┌─┐┌┐┌┌─┐┌─┐    "
echo " ││││ │││││ ┬│ │     "
echo " ┴ ┴└─┘┘└┘└─┘└─┘    "
echo " ┌─┐┌┐┌               "
echo " │ ││││               "
echo " └─┘┘└┘               "
echo " ┌┬┐┌─┐┌─┐┬┌─┌─┐┬─┐ "
echo "  │││ ││  ├┴┐├┤ ├┬┘  "
echo " ─┴┘└─┘└─┘┴ ┴└─┘┴└─ "
echo " run mongo --username admin --password password for authenticated mongo shell"

# open bash in contaner to connect to mongo shell
docker exec -it mongodb bash
Enter fullscreen mode Exit fullscreen mode

Now we can add these to your profile (.bashrc, .zshrc, .profile or .aliases however you're doing it) and source your profile script to ensure these new aliases are part of the env. You can now start them with easier to remember commands you've created yourself. Hurrah!

Now we've got docker installed, mongo pulled and the ability to bring it up and connect to the shell with our own wizardry we can say that's section 1 of the post complete.

Go, Go Go....

go go go

Now we move to the Go part. We'll be using the go-mongo driver, you can get this by creating your project folder and running your go mod init github.com/username/projectname command and then running the go get for your dependencies.

  • go get go.mongodb.org/mongo-driver/mongo
  • go get -u github.com/gorilla/mux
  • go get gopkg.in/mgo.v2/bson

We're good to go. (see what I did there?)

Get connected

OK let's get the main.go working.

package main

import (
    "context"
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "time"

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

type DB struct {
    collection *mongo.Collection
}

func main() {

    // dont put this in real code. Nobody will speak to you
    credentials := options.Credential{
        Username: "admin",
        Password: "password",
    }

    clientOptions := options.Client().ApplyURI("mongodb://localhost:27017").SetAuth(credentials)
    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}

    r := mux.NewRouter()
    r.HandleFunc("/v1/movies/{id:[a-zA-Z0-9]*}", db.GetMovie).Methods(http.MethodGet)
    r.HandleFunc("/v1/movies/{id:[a-zA-Z0-9]*}", db.UpdateMovie).Methods(http.MethodPut)
    r.HandleFunc("/v1/movies/{id:[a-zA-Z0-9]*}", db.DeleteMovie).Methods(http.MethodDelete)
    r.HandleFunc("/v1/movies", db.PostMovie).Methods(http.MethodPost)

    srv := &http.Server{
        Handler:      r,
        Addr:         "localhost:8080",
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
    }

    log.Fatal(srv.ListenAndServe())
}
Enter fullscreen mode Exit fullscreen mode

Things to note:

  1. This is a demo, for the love of god don't put your credentials into any "real" code like I have done in this above, you'll get blasted for it (rightly so), this is just for demo purposes. You'll want to put that sort of stuff into env variables and read them in.
  2. read #1 again!
  3. Note the DB structure. That's for convenience.
  4. We're using gorilla/mux to define and configure our multiplexer.
  5. hark at me with my fancy regex to grab any alphanumeric part of the url for matching IDs.

Next up we're going to cut some CRUD functions to be able to do some stuff with or database.

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)
    }
}

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.Write(response)
    }
}

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}
    result, err := db.collection.UpdateOne(context.TODO(), filter, update)
    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.Write(response)
    }
}

func (db *DB) DeleteMovie(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    objectID, _ := primitive.ObjectIDFromHex(vars["id"])
    filter := bson.M{"_id": objectID}

    result, err := db.collection.DeleteOne(context.TODO(), filter)
    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.Write(response)
    }
}
Enter fullscreen mode Exit fullscreen mode

things to note:

  1. There's a lot in these functions that could be outsourced to clean the functions and reduce code. Outside the scope of this basic connect and get a quick and dirty demo together.
  2. We can enhance the use of context to avoid using the .TODO context which is a default.

Lastly we need to add the structs to make up a movie entity.

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"`
}
Enter fullscreen mode Exit fullscreen mode

Things to note:

  1. we define the json and bson names/refs
  2. we have a relationship between movie and BoxOffice.
  3. we have attributes that take a slice of values.

you can run the server and tap into this with some postman requests or simply with some curl requests. That choice is yours. That's hopefully enough for you to see how this works without focusing too much on code beauty by adding more abstraction at this stage.

Quick & dirty but I hope if you need a quick smash & grab demo of using the mongo driver for Go with gorilla mux then it should be enough to get you started on the right path with one eye on creating cleaner, better code as you get to grips with it..... ps. don't embed credentials in code outside of a demo.

Best wishes.

💖 💪 🙅 🚩
iamelesq
elesq

Posted on August 2, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related