Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Build A Full Stack Movie Database With Golang, Angular, And NoSQL

TwitterFacebookRedditLinkedInHacker News

With all the technologies and platforms available, it opens the door to infinite possibilities for development and further validates the need of being a full stack developer. There are many stacks in existence, but one of my personal favorites includes Golang, Angular, and NoSQL.

So how do you apply all these stack technologies towards a fully functional application? Let’s look at a possible usage scenario before we explore the technologies.

A problem I’ve found myself having recently is keeping track of all my movies. Can you believe I’ve purchased the same film multiple times by accident? From this spawned my need to keep a database of every movie I purchased. Using NoSQL, Angular, and the Go programming language, we can create such an application to keep track of what films we own and for what platforms.

Now this isn’t the first time I’ve explored the Golang, NoSQL, or even Angular on The Polyglot Developer. Take for example, my tutorial on creating a URL shortener or using websockets with Go and Angular. These tutorials are great, but they are a bit fragmented. For example, my URL shortener guide doesn’t include a UI and my websocket guide covers very complicated websockets.

We’re going to combine the best of both worlds here. Take the following animated image of our soon to be created, fully functional web application:

Golang Movie Database

In the above example we have two screens and a lot going on.

First and probably the most noticeable is the fact that we’re using the theming package, Material2. It is similar to Bootstrap, but uses material design and is meant for Angular. The second thing you’ll notice in the application is that we’re using Angular for all of our front-end. This front-end will make requests against our Golang backend which is powered by NoSQL.

In this example we’ll be using the open source database, Couchbase. To be honest, every technology we’re using in this tutorial is open source. We’re using Couchbase because it is a solid database and what I’m most familiar with.

The Requirements

There are a lot of moving parts when it comes to this application, but they aren’t difficult to keep track of. To be successful, you’ll need the following installed:

  • Couchbase Server 4.5+
  • Golang 1.7+
  • Node.js 6.0+

When it comes to Couchbase Server, it doesn’t matter if you’re using Community Edition or Enterprise Edition. At the end of this tutorial, you don’t even have to use Couchbase if you don’t want to. It is a starting point towards being a successful developer on your own. We need Node.js because it ships with the Node Package Manager (NPM), which is a requirement when building Angular applications.

Preparing the NoSQL Database, Couchbase

At this point you should have already installed and configured Couchbase Server. We’re going to need at least one Bucket created with some special indexes for querying.

From the administrative dashboard, found at http://:8091, click the Data Buckets tab and choose Create New Data Bucket. You’ll need to give the Bucket a name and define a storage capacity.

Couchbase Server Create Bucket

I’ll be calling my bucket, example, but feel free to name it whatever you’d like.

Getting ahead of ourselves, we plan to query our NoSQL database using a variety of querying techniques. One of these techniques includes a technology called N1QL which requires certain indexes to exist on the Bucket.

CREATE PRIMARY INDEX ON `example` USING GSI;

Using the Couchbase Query Workbench or the Couchbase Shell (CBQ), execute the above query. This will create the most basic index possible.

Couchbase Server Create Primary Index

When you start dealing with massive amounts of data and complex queries, it would make sense to define more specific indexes for each job. Our generic index will be quite slow under those scenarios.

The Movie Database Data Model

While this isn’t a configuration step, it is important to understand our data model for the database. Each movie will represent a single JSON document within the Couchbase Bucket. NoSQL offers great flexibility, so these documents can be modeled as anything. However, our model will look like the following:

{
    "name": "Star Wars: Force Awakens",
    "genre": "Action / Science Fiction",
    "formats": {
        "digital": true,
        "bluray": true,
        "dvd": true
    }
}

Every movie will have a name and genre that will be filled through user input. If you’re like me, you’ve been collecting movies for a while and may have them in different formats. These formats will be nested for cleanliness and be boolean values.

Again, this is NoSQL and you can throw together your JSON documents however you want. It wouldn’t be this easy in a relational database like MySQL.

Creating a New Server-Side Golang Project

It makes sense, and is easier, to create a backend before worrying about the front-end. This is because when developing the front-end, the only thing you really ever care about is data. This is data that the backend will provide to us.

For simplicity, our backend will be composed of three RESTful API endpoints. This will allow us to list all our movies, search for movies, and add new movies to the database.

Before we jump into the code, we need to download all of our Golang dependencies. Using the Command Prompt or Terminal, execute the following:

go get github.com/couchbase/gocb
go get github.com/gorilla/handlers
go get github.com/gorilla/mux
go get github.com/satori/go.uuid

The above will get the Couchbase Go SDK, a library for making RESTful API endpoints easier to create, a library for handling cross origin resource sharing (CORS) and a library for generating unique id values that will represent NoSQL document keys.

All of our Golang development will happen in a single file. Create and open a $GOPATH/src/github.com/nraboy/fullstack/main.go file and include the following code:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strings"

    "github.com/couchbase/gocb"
    "github.com/gorilla/handlers"
    "github.com/gorilla/mux"
    "github.com/satori/go.uuid"
)

type Movie struct {
    ID      string      `json:"id,omitempty"`
    Name    string      `json:"name,omitempty"`
    Genre   string      `json:"genre,omitempty"`
    Formats MovieFormat `json:"formats,omitempty"`
}

type MovieFormat struct {
    Digital bool `json:"digital,omitempty"`
    Bluray  bool `json:"bluray,omitempty"`
    Dvd     bool `json:"dvd,omitempty"`
}

var bucket *gocb.Bucket
var bucketName string

func ListEndpoint(w http.ResponseWriter, req *http.Request) { }

func SearchEndpoint(w http.ResponseWriter, req *http.Request) { }

func CreateEndpoint(w http.ResponseWriter, req *http.Request) { }

func main() {
    fmt.Println("Starting server at http://localhost:12345...")
    cluster, _ := gocb.Connect("couchbase://localhost")
    bucketName = "example"
    bucket, _ = cluster.OpenBucket(bucketName, "")
    router := mux.NewRouter()
    router.HandleFunc("/movies", ListEndpoint).Methods("GET")
    router.HandleFunc("/movies", CreateEndpoint).Methods("POST")
    router.HandleFunc("/search/{title}", SearchEndpoint).Methods("GET")
    log.Fatal(http.ListenAndServe(":12345", handlers.CORS(handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD"}), handlers.AllowedOrigins([]string{"*"}))(router)))
}

The above code is essential boilerplate logic for our project.

Skipping past the imports, we have a few custom data structures. The Movie and MovieFormat data structures will be mapped directly with the data that resides in our database. It is safe to say that previously we created a database data model and now we’re creating an application data model.

Jump to the main function and you’ll see that we’re establishing a connection to our NoSQL database and opening our freshly created Bucket. After, we define our three endpoints and link them to functions where we’ll execute some particular database related logic.

When we start our server we are defining a port and passing in some cross origin resource sharing (CORS) rules. Basically we are saying we want to serve our server-side application at http://localhost:12345 and accept requests from anyone, and from anywhere. This is necessary because our Angular application will run on a different port.

Looking at our endpoint functions, let’s start with probably the easiest. When it comes to adding new movies, we just want to create new documents:

func CreateEndpoint(w http.ResponseWriter, req *http.Request) {
    var movie Movie
    _ = json.NewDecoder(req.Body).Decode(&movie)
    bucket.Insert(uuid.NewV4().String(), movie, 0)
    json.NewEncoder(w).Encode(movie)
}

In the CreateEndpoint function we take the POST body that was passed from the client and load it into a Movie object. Using the UUID library we can create a unique value to represent our key and save it to the database along with the Movie object. For convenience we are just going to return the film that was inserted.

Now what if we wanted to return all the documents currently in the database?

func ListEndpoint(w http.ResponseWriter, req *http.Request) {
    var movies []Movie
    query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "`")
    query.Consistency(gocb.RequestPlus)
    rows, _ := bucket.ExecuteN1qlQuery(query, nil)
    var row Movie
    for rows.Next(&row) {
        movies = append(movies, row)
        row = Movie{}
    }
    if movies == nil {
        movies = make([]Movie, 0)
    }
    json.NewEncoder(w).Encode(movies)
}

In the above ListEndpoint function we are working towards a N1QL query. The results of this query will be stored in a slice of our Movie object, serialized to JSON, and then returned to the client.

This endpoint will be called when loading our application and refreshing our data after we add new data. Because the refresh happens so quickly after adding new data, we need to wait until the index has been updated, otherwise we might get stale data.

query.Consistency(gocb.RequestPlus)

Defining our own query consistency lets us accommodate these scenarios. More information on query consistency can be found in the Couchbase documentation.

This brings us to the final API endpoint function. We need a way to search for movies instead of returning everything in our database.

func SearchEndpoint(w http.ResponseWriter, req *http.Request) {
    var movies []Movie
    params := mux.Vars(req)
    var n1qlParams []interface{}
    n1qlParams = append(n1qlParams, strings.ToLower(params["title"]))
    query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE LOWER(name) LIKE '%' || $1 || '%'")
    query.Consistency(gocb.RequestPlus)
    rows, _ := bucket.ExecuteN1qlQuery(query, n1qlParams)
    var row Movie
    for rows.Next(&row) {
        movies = append(movies, row)
        row = Movie{}
    }
    if movies == nil {
        movies = make([]Movie, 0)
    }
    json.NewEncoder(w).Encode(movies)
}

You’ll notice that the SearchEndpoint function is nearly the same as the previous used for listing all the currently saved documents. The exception here is that we’re extracting data from the URL parameters and defining a parameterized N1QL query that uses the data. By doing this, we can accept user generated values and use them without the risk of a SQL injection attack.

Give the Golang application a try now. From the Command Prompt or Terminal, execute the following:

go run *.go

When you plan to distribute the application you’ll build it, but running it for now is fine. With the server running, it can be accessed at http://localhost:12345. You can even test it with tools like Postman.

Building the Client Facing Angular Application

With the backend out of the way, we can focus on building an attractive front-end for consuming the RESTful API data. Angular will be the basis of this section, so we’ll need to have the Angular CLI available.

If you don’t already have it, execute the following:

npm install -g angular-cli

With the Angular CLI in hand, we want to create a new project by executing the following:

ng new AngularProject

This may take a while, but when done we’ll have a simple Angular application. This application will use the theming layer called Material2 which can be installed via the Node Package Manager (NPM).

Navigate into your project and execute the following command:

npm install @angular/material --save

With the project created and our theming layer installed, we need to create our two application pages. Remember, we’re creating a two page application.

From the Terminal or Command Prompt, execute the following:

ng g component create
ng g component movies

The above commands will create two components using the Angular CLI generators. These components include TypeScript, HTML, and CSS files.

We have the components, but they are orphaned from the rest of our application and each other. What we need to do is define Angular routes for navigating to these components. Create and open a src/app/app.routing.ts file and include the following TypeScript code:

import { MoviesComponent } from "./movies/movies.component";
import { CreateComponent } from "./create/create.component";

export const AvailableRoutes: any = [
    { path: "", component: MoviesComponent },
    { path: "create", component: CreateComponent }
];

In the above code we’re defining two routes and describing how to get there. The route with an empty path is the default route and the first to appear on the screen. Beyond that, there is no particular order or naming convention.

These routes need to be added to the project’s @NgModule block. Open the project’s src/app/app.module.ts file and include the following code:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';
import { MaterialModule } from '@angular/material';
import 'hammerjs';

import { AvailableRoutes } from './app.routing';

import { AppComponent } from './app.component';
import { MoviesComponent } from './movies/movies.component';
import { CreateComponent } from './create/create.component';

@NgModule({
    declarations: [
        AppComponent,
        MoviesComponent,
        CreateComponent
    ],
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        RouterModule,
        RouterModule.forRoot(AvailableRoutes),
        MaterialModule.forRoot()
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

In the above file we’ve imported the routing file and added it to the imports section of the @NgModule block. We’re also importing Material2, and configuring it as well, per the Material2 documentation.

If you wish to use a pre-built Material2 theme, which you probably do, include the following line in the project’s src/styles.css file:

@import '~@angular/material/core/theming/prebuilt/deeppurple-amber.css';

One more thing needs to be done before we can start adding page logic and UI markup. We need to define the pass-through for component routes.

Open the project’s src/app/app.component.html file and include the following markup:

<router-outlet></router-outlet>

Now our route HTML files will pass through the above outlet for display on the screen.

Creating the Movie Listing Page Logic and UI

We’re going to work on the TypeScript, CSS, and HTML for the first of our two pages. Starting with the src/app/movies/movies.component.ts, open it and include the following TypeScript code:

import { Component, OnInit } from '@angular/core';
import { Http } from '@angular/http';
import { Router } from '@angular/router';
import { Location } from '@angular/common';

@Component({
    selector: 'app-movies',
    templateUrl: './movies.component.html',
    styleUrls: ['./movies.component.css']
})
export class MoviesComponent implements OnInit {

    public movies: any;

    public constructor(private http: Http, private router: Router, private location: Location) {
        this.movies = [];
    }

    public ngOnInit() {
        this.location.subscribe(() => {
            this.refresh();
        });
        this.refresh();
    }

    private refresh() {
        this.http.get("http://localhost:12345/movies")
            .map(result => result.json())
            .subscribe(result => {
                this.movies = result;
            });
    }

    public search(event: any) {
        let url = "http://localhost:12345/movies";
        if(event.target.value) {
            url = "http://localhost:12345/search/" + event.target.value;
        }
        this.http.get(url)
            .map(result => result.json())
            .subscribe(result => {
                this.movies = result;
            });
    }

    public create() {
        this.router.navigate(["create"]);
    }

}

There is a lot going on in the above code so we’re going to break it down.

Within the MoviesComponent class we have a public variable that will be bound to the UI. This will hold all movies returned in our Golang request. In the constructor method we are initializing this variable and injecting a few Angular services that will be used throughout the page.

When the page loads, we want to load our remote data as well.

private refresh() {
    this.http.get("http://localhost:12345/movies")
        .map(result => result.json())
        .subscribe(result => {
            this.movies = result;
        });
}

In the refresh method we do an HTTP request against our Golang API. The result will be transformed into a JSON object and added to our public array.

This refresh method is called in two locations, both of which are in the ngOnInit method. Data should never be loaded in the constructor method, but instead the ngOnInit method that is triggered after.

public ngOnInit() {
    this.location.subscribe(() => {
        this.refresh();
    });
    this.refresh();
}

The reason we have a subscription to the location is because we want to be able to identify navigation events backwards in the stack. After we save data we want to know that we’ve navigated backwards so we can refresh the screen.

This brings us to the search method:

public search(event: any) {
    let url = "http://localhost:12345/movies";
    if(event.target.value) {
        url = "http://localhost:12345/search/" + event.target.value;
    }
    this.http.get(url)
        .map(result => result.json())
        .subscribe(result => {
            this.movies = result;
        });
}

The event parameter will hold the user input from the UI. If it is empty when it is submitted, just load all data, otherwise consume the data from the SearchEndpoint of the Golang code. In either scenario, the results are added to the public array and displayed on the screen.

The final method in this file, create, will take us to the second screen.

So what does the UI look like for this page?

Open the project’s src/app/movies/movies.component.html file and include the following HTML markup:

<md-toolbar color="primary">
    NG2 Movie Database
    <span class="app-toolbar-filler"></span>
    <button md-button (click)="create()">New</button>
</md-toolbar>
<div class="app-content">
    <div>
        <md-input placeholder="Search" (keyup.enter)="search($event)"></md-input>
    </div>
    <table style="width: 100%; margin-top: 10px; border-collapse: collapse">
        <thead style="text-align: left">
            <tr>
                <th>Title</th>
                <th>Genre</th>
                <th>Digital</th>
                <th>Blu-Ray</th>
                <th>DVD</th>
            </tr>
        </thead>
        <tbody>
            <tr *ngFor="let movie of movies" style="border-top: 1px solid black">
                <td>{{ movie.name }}</td>
                <td>{{ movie.genre }}</td>
                <td><md-checkbox [disabled]="true" [checked]="movie.formats.digital || false"></md-checkbox></td>
                <td><md-checkbox [disabled]="true" [checked]="movie.formats.bluray || false"></md-checkbox></td>
                <td><md-checkbox [disabled]="true" [checked]="movie.formats.dvd || false"></md-checkbox></td>
            </tr>
        </tbody>
    </table>
</div>

In the toolbar we have a button that when clicked will call the create method and take us to the second screen. There is a search input field and when the enter key is pressed, the value will be passed to the search method.

In the table, we loop through the movies array presenting each property of the objects on the screen. Most of the theming is part of the Material2 package, but we do have a few custom CSS properties. Open the project’s src/app/movies/movies.component.css file and include the following CSS:

.app-toolbar-filler {
    flex: 1 1 auto;
}

th {
    padding: 10px 0px;
}

td {
    padding: 10px 0px;
}

The first page of our application is now complete!

Creating the Page Logic and UI for Adding New Movies

The final page of our application will be similar to what we’ve already seen. Open the project’s src/app/create/create.component.ts file and include the following TypeScript code:

import { Component, OnInit } from '@angular/core';
import { Location } from '@angular/common';
import { Http } from '@angular/http';

@Component({
    selector: 'app-create',
    templateUrl: './create.component.html',
    styleUrls: ['./create.component.css']
})
export class CreateComponent implements OnInit {

    public movie: any;

    public constructor(private location: Location, private http: Http) {
        this.movie = {
            "name": "",
            "genre": "",
            "formats": {
                "digital": false,
                "bluray": false,
                "dvd": false
            }
        }
    }

    public ngOnInit() { }

    public save() {
        if(this.movie.name && this.movie.genre) {
            this.http.post("http://localhost:12345/movies", JSON.stringify(this.movie))
                .subscribe(result => {
                    this.location.back();
                });
        }
    }

}

In the CreateComponent class we have a public variable that will be bound to a form on the UI. This variable is initialized in the constructor method along with various Angular service injections.

We are not loading any data so we are not using the ngOnInit method.

public save() {
    if(this.movie.name && this.movie.genre) {
        this.http.post("http://localhost:12345/movies", JSON.stringify(this.movie))
            .subscribe(result => {
                this.location.back();
            });
    }
}

When we try to save, we make sure some of the data is populated and then POST to the Golang API. Once successful we can navigate backwards in the stack.

The UI found in the src/app/create/create.component.html file will look like the following:

<md-toolbar color="primary">
    NG2 Movie Database
    <span class="app-toolbar-filler"></span>
    <button md-button (click)="save()">Save</button>
</md-toolbar>
<div class="app-content">
    <md-card>
        <md-card-header>
            <md-card-title>Bought a new Movie?</md-card-title>
        </md-card-header>
        <md-card-content>
            <md-input placeholder="Name" [(ngModel)]="movie.name"></md-input><br />
            <md-input placeholder="Genre" [(ngModel)]="movie.genre"></md-input><br />
            <md-checkbox [checked]="false" [(ngModel)]="movie.formats.digital">Digital Copy</md-checkbox>
            <md-checkbox [checked]="false" [(ngModel)]="movie.formats.bluray">Blu-Ray</md-checkbox>
            <md-checkbox [checked]="false" [(ngModel)]="movie.formats.dvd">DVD</md-checkbox>
        </md-card-content>
    </md-card>
</div>

Like with the other page, we have a button, that when pressed will call our public method. Each of the input fields on this screen is bound to the TypeScript file through the [(ngModel)] tags.

Finally, the CSS that goes with this HTML file will look like the following:

.app-toolbar-filler {
    flex: 1 1 auto;
}

The above CSS is found in the project’s src/app/create/create.component.css file.

At this point the Angular front-end to our full stack application is now complete. You should be proud of yourself!

Taking the Application for a Test Drive

When it comes to running our applications, you saw how to run the Golang application. Just to reiterate, execute the following from the command line:

go run *.go

The server side application will now be serving at http://localhost:12345. To run the Angular application, we’ll want to run the following:

ng serve

This Angular application will be serving at http://localhost:4200, but since we set up our CORS logic, it will be able to communicate with our Golang application.

Don’t want to go through the entire tutorial to see results? Download the full project source code here, and execute the following within the Angular project:

npm install

The above command will install all the Angular dependencies.

Conclusion

You just saw how to build a full stack application that made use of Golang, Angular, and a NoSQL database, all of which are open source and free to use. You can do a lot of great things will all of these technologies, whether it be individually or all together. For extra help you can visit my URL shortener tutorial or Golang websocket tutorial.

Don’t forget, as a premium member, you can access the full project source code here.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.