Not too long ago you’ll remember I wrote a full stack tutorial on developing a full stack movie database with the Go programming language. In that tutorial we made use of NoSQL as the database, Golang as the backend, and Angular as the client frontend. However, I realize that not everyone is a Go developer.
This time around we’re going to take a look at developing the same full stack movie database application, but using Node.js instead of Golang. It is a good example to show that elements in the stack are modular and each element is replaceable with another technology.
Now this definitely isn’t the first time we explored Node.js, NoSQL, or Angular on The Polyglot Developer. If you remember, we previously created a simple application with Node.js and Couchbase Server as well as a RESTful API with Node.js. We’re going to be taking it further this time around while including Angular in the stack.
Take the following animated image as an example of what we plan to build.
In the above example we have two different screens and a lot going on with Angular and HTML.
First you’ll probably notice that we’re using Bootstrap as our theme. It is not designed for Angular, but we make it work anyways. Everything from the image is entirely frontend. What is happening is the Angular application is communicating to Node.js via RESTful API requests and the Node.js backend is communicating with NoSQL.
In this example we’ll be using the open source NoSQL database, Couchbase, because it’s a solid technology and it’s what I’m most familiar with. To be clear, everything in this tutorial will be open source.
There are a few software requirements in order to make this project a success. Make sure that you’ve downloaded the following:
When it comes to Couchbase Server, it doesn’t matter if you’re using Community Edition or Enterprise Edition. I’m using version 4.5, so it is a good idea to use at a minimum the same as me. However, something newer should be fine as well. Node.js is required because it comes with the Node Package Manager (NPM) which is required when building Node.js and Angular applications.
By now you should have already installed and configured Couchbase Server. We’re going to spend some time getting Couchbase prepared for our project.
Each application you build will need its own Bucket in Couchbase. This Bucket is where all the NoSQL data for your application will reside. From the administrative dashboard found at http://:8091, click the Data Buckets tab at the top and choose the Create New Data Bucket option. You’ll need to give the Bucket a name and define some of the settings.
In this tutorial I’ll be referring to our Bucket name as example, but you can name it whatever you’d like.
Getting ahead of ourselves, we’ll be using N1QL with Couchbase when it comes to executing queries. This adds a requirement of placing indexes on our Bucket.
CREATE PRIMARY INDEX ON example USING GSI;
The above query to create an index can either be executed using Query Workbench found in Enterprise Edition or the Couchbase Shell (CBQ) found in both versions. Executing this query will create the most basic index.
When you start dealing with massive amounts of data or more complex queries, you’ll want to create more specific indexes. Our generic index will be quite slow for complicated situations. This tutorial won’t need anything specific.
Before using the database, it is probably a good idea to understand the data model that will be used within our movie application. Each movie will represent a single JSON document in the database. NoSQL offers great flexibility so these JSON documents can be anything, but for our specific use-case, they will be modeled as the following:
{
"name": "Star Trek Beyond",
"genre": "Action / Science Fiction",
"formats": {
"digital": true,
"bluray": true,
"dvd": true
}
}
Each movie will have a name
and genre
that are populated through user input. If you’re like me, you probably have movies in all different formats, which is why there is a nested formats
object.
Again, the power of NoSQL is in its flexibility amongst other things. It would be a lot more difficult to do this with a relational database.
The backend Node.js project that we’re going to create has a few development dependencies. Create a new directory somewhere on your computer and navigate into it with the Command Prompt (Windows) or Terminal (Mac and Linux). We’re going to want to execute the following command.
npm init --y
The above command will create a basic package.json file that will eventually hold our dependency information. To add our dependencies, we’ll want to run the following:
npm install body-parser --save
npm install cors --save
npm install couchbase --save
npm install express --save
npm install uuid --save
So what do each of these dependencies do? This Node.js project will use the Express framework. Because it is a RESTful application, we’re going to be sending request bodies, hence the need for the body-parser
package. The Angular application will run on a different port than the Node.js application so we need to enable cross origin resource sharing (CORS). Finally, to generate unique data keys, it is a good idea to use a UUID.
After running the commands you’ll end up with a node_modules directory. Within the project, create an app.js file which will hold all of our application code.
The code found in the app.js file will look like the following:
var Couchbase = require("couchbase");
var BodyParser = require("body-parser");
var Uuid = require("uuid");
var Cors = require("cors");
var Express = require("express");
var app = Express();
var N1qlQuery = Couchbase.N1qlQuery;
app.use(BodyParser.json());
app.use(Cors())
var bucket = (new Couchbase.Cluster("couchbase://localhost")).openBucket("example");
app.get("/movies", function(req, res) {});
app.get("/movies/:title", function(req, res) {});
app.post("/movies", function(req, res) {});
app.listen(3000, function() {
console.log("Starting server on port 3000...");
});
So what is happening in the above code?
Well first we are importing each of the dependencies that we had previously installed. After importing the dependencies, we initialize Express framework and configure CORS as well as enable JSON request bodies.
Before we can start saving and querying data, we need to establish a connection to Couchbase and open the Bucket that we had created. The endpoint functions that hit Couchbase are blank for now, but we’ll get to them next. After all the configuration we start running our Node.js application on port 3000.
So what do each of the three endpoint functions look like?
The first and probably easiest endpoint function is the endpoint for returning all movie data back to the client. This endpoint looks like the following:
app.get("/movies", function(req, res) {
var query = N1qlQuery.fromString("SELECT example.* FROM example").consistency(N1qlQuery.Consistency.REQUEST_PLUS);
bucket.query(query, function(error, result) {
if(error) {
return res.status(400).send({ "message": error });
}
res.send(result);
});
});
In the above endpoint we are constructing a SQL-like query using the N1QL feature found in Couchbase. Essentially we are saying we want all properties from all documents returned. After executing the query, either an error or the data will be returned.
How about creating new data? Instead of using a GET endpoint we’ll be using a POST endpoint that looks like the following:
app.post("/movies", function(req, res) {
if(!req.body.name) {
return res.status(400).send({ "message": "Missing `name` property" });
} else if(!req.body.genre) {
return res.status(400).send({ "message": "Missing `genre` property" });
}
bucket.insert(Uuid.v4(), req.body, function(error, result) {
if(error) {
return res.status(400).send({ "message": error });
}
res.send(req.body);
});
});
In the above function we check the data that comes in. If a name
and genre
property do not exist, an error will be returned. Otherwise, data with a unique key will be inserted into our Bucket.
Finally we have an endpoint for searching for data. We can provide a movie title and get any results that are similar.
app.get("/movies/:title", function(req, res) {
if(!req.params.title) {
return res.status(400).send({ "message": "Missing `title` parameter" });
}
var query = N1qlQuery.fromString("SELECT example.* FROM example WHERE LOWER(name) LIKE '%' || $1 || '%'").consistency(N1qlQuery.Consistency.REQUEST_PLUS);
bucket.query(query, [req.params.title.toLowerCase()], function(error, result) {
if(error) {
return res.status(400).send({ "message": error });
}
res.send(result);
});
});
This query is slightly different than our previous query. This time we are executing a parameterized query. We are doing this because we don’t want to suffer from a SQL injection attack from malicious user data.
At this point our Node.js backend is complete and is ready to be used by a client facing application. If you want to test the API before moving along, you might want to check out Postman.
With the backend out of the way, we can focus on a client facing application for consuming and creating the data. This client facing application will be created with Angular, HTML, and CSS.
To use Angular we need to have the Angular CLI installed. This can be done through NPM with the following:
npm install -g angular-cli
If you’re using a Mac or Linux computer, you might need to include a sudo
in the above command.
With the Angular CLI installed, we can create a new Angular project. This can be done by executing the following command:
ng new angular
The above command will create a new project in an angular directory. The command may take a while to complete because Angular has a massive amount of development dependencies.
With the project created we can configure Bootstrap to be used throughout our application for theming. While there are NPM modules available, we’re going to have more success adding it manually. Open the project’s src/index.html file and include the following:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Angular</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<app-root>Loading...</app-root>
</body>
</html>
Notice that we’ve included the Bootstrap CSS and JavaScript through the CDN.
Now we can focus on creating each of the application pages. The CLI has many conveniences, including being able to generate application pages.
Using the Terminal or Command Prompt, execute the following:
ng g component create
ng g component movies
The above commands will create appropriate TypeScript, HTML, and CSS files for the project.
There are a few more bootstrapping steps required before we can start developing each of our application pages. While we’ve added two new pages, they are orphaned until we connect them to the application.
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 { AppComponent } from './app.component';
import { CreateComponent } from './create/create.component';
import { MoviesComponent } from './movies/movies.component';
let routes: any = [
{ path: "", component: MoviesComponent },
{ path: "create", component: CreateComponent }
];
@NgModule({
declarations: [
AppComponent,
CreateComponent,
MoviesComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule,
RouterModule.forRoot(routes)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
In the above code we’ve imported the RouterModule
and defined two page routes, the default being the one with the empty path. These routes and the routing module are configured in the imports
array of the @NgModule
block.
To make routing possible, we need an outlet of rendering. All child pages need to pass through this routing outlet. This can be accomplished within the project’s src/app/app.component.html file. Open this file and include the following line:
<router-outlet></router-outlet>
Now we can focus on each of the application pages.
We’re going to depend on Bootstrap for all CSS, so the TypeScript and HTML are what matters to us here. Starting with the project’s src/app/movies/movies.component.ts file, open it and include the following:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Http } from '@angular/http';
import { Location } from '@angular/common';
import "rxjs/Rx";
@Component({
selector: 'app-movies',
templateUrl: './movies.component.html',
styleUrls: ['./movies.component.css']
})
export class MoviesComponent implements OnInit {
public movies: any;
public constructor(private router: Router, private http: Http, private location: Location) {
this.movies = [];
}
public ngOnInit() {
this.location.subscribe(() => {
this.refresh();
});
this.refresh();
}
public refresh(query?: any) {
let url = "http://localhost:3000/movies";
if(query && query.target.value) {
url = "http://localhost:3000/movies/" + query.target.value;
}
this.http.get(url)
.map(result => result.json())
.subscribe(result => {
this.movies = result;
});
}
public create() {
this.router.navigate(["create"]);
}
}
In the above MoviesComponent
class we have a lot going on.
We have a public movies
variable which will hold all available movies and eventually render them to the screen. In the constructor
method we initialize this variable as well as inject various Angular services for use.
When it comes to consuming our API data, we have the refresh
method:
public refresh(query?: any) {
let url = "http://localhost:3000/movies";
if(query && query.target.value) {
url = "http://localhost:3000/movies/" + query.target.value;
}
this.http.get(url)
.map(result => result.json())
.subscribe(result => {
this.movies = result;
});
}
If a query value was passed we know we are trying to search for a movie, otherwise we just want to list all movies. The HTTP request will hit our endpoint and the result will be transformed and added to our public array.
It is never a good idea to load data in the constructor
method, so instead we call the refresh
method within the ngOnInit
method which triggers after the constructor
method.
public ngOnInit() {
this.location.subscribe(() => {
this.refresh();
});
this.refresh();
}
Because the ngOnInit
only triggers when navigated to, we need a way to refresh the page when navigating back. This is where the location.subscribe
comes into play.
So how does the UI look? Open the project’s src/app/movies/movies.component.html file and include the following HTML markup:
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Movie Database Example</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li>
<form class="navbar-form">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search" (keyup.enter)="refresh($event)">
</div>
</form>
</li>
<li><a style="cursor: pointer" (click)="create()">Add Movie</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">Movies</div>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Genre</th>
<th>Digital Copy</th>
<th>Blu-Ray</th>
<th>DVD</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let movie of movies">
<td>{{ movie.name }}</td>
<td>{{ movie.genre }}</td>
<td>{{ movie.formats.digital || false }}</td>
<td>{{ movie.formats.bluray || false }}</td>
<td>{{ movie.formats.dvd || false }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
There is a lot of HTML in the above, but most of the important stuff is in the table content. We loop through our array and display each object on the screen.
Time to move onto the second page of the application.
The final page isn’t too different from what we’ve already seen. We’ll be working with a TypeScript file and an HTML file for the UI.
Open the project’s src/app/create/create.component.ts file and include the following TypeScript code:
import { Component, OnInit } from '@angular/core';
import { Http, Headers, RequestOptions } from '@angular/http';
import { Location } from '@angular/common';
@Component({
selector: 'app-create',
templateUrl: './create.component.html',
styleUrls: ['./create.component.css']
})
export class CreateComponent implements OnInit {
public movie: any;
public constructor(private http: Http, private location: Location) {
this.movie = {
"name": "",
"genre": "",
"formats": {
"digital": false,
"bluray": false,
"dvd": false
}
};
}
public ngOnInit() { }
public save() {
if(this.movie.name && this.movie.genre) {
let headers = new Headers({ "Content-Type": "application/json" });
let options = new RequestOptions({ "headers": headers });
this.http.post("http://localhost:3000/movies", JSON.stringify(this.movie), options)
.subscribe(result => {
this.location.back();
});
}
}
}
In the CreateComponent
we have another public variable, but this time it will be bound to our web form. In the constructor
method we initialize this variable and inject a few Angular services.
We aren’t loading any data so we don’t make use of the ngOnInit
method, but we are saving data, hence the save
method:
public save() {
if(this.movie.name && this.movie.genre) {
let headers = new Headers({ "Content-Type": "application/json" });
let options = new RequestOptions({ "headers": headers });
this.http.post("http://localhost:3000/movies", JSON.stringify(this.movie), options)
.subscribe(result => {
this.location.back();
});
}
}
Because we are doing a POST instead of a GET, we need to define the format that we are sending. With headers we can define the data as JSON.
So what does the UI look like for this page?
Open the project’s src/app/create/create.component.html file and include the following HTML markup:
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Movie Database Example</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li><a style="cursor: pointer" (click)="save()">Save</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">Bought a Movie?</div>
<div class="panel-body">
<div class="form-group">
<label for="title">Title</label>
<input type="text" class="form-control" id="title" placeholder="Title" [(ngModel)]="movie.name">
</div>
<div class="form-group">
<label for="genre">Genre</label>
<input type="text" class="form-control" id="genre" placeholder="Genre" [(ngModel)]="movie.genre">
</div>
<div class="checkbox">
<label>
<input type="checkbox" [(ngModel)]="movie.formats.digital"> Digital Copy
</label>
<label>
<input type="checkbox" [(ngModel)]="movie.formats.bluray"> Blu-Ray
</label>
<label>
<input type="checkbox" [(ngModel)]="movie.formats.dvd"> DVD
</label>
</div>
</div>
</div>
</div>
</div>
</div>
What matters in the above is the form. Each of the form elements is bound to our public TypeScript variable through the [(ngModel)]
tags. This is a two way data binding so changing the data will reflect either the TypeScript or the HTML.
Just like that, the client facing Angular application is now complete!
For this example we are not bundling the Angular and Node.js together. Instead we will run them as separate applications. To run the Node.js application, execute the following:
node app.js
The Node.js application will be accessible at http://localhost:3000, but will have no frontend. To run the Angular frontend, we want to execute the following:
ng serve
The Angular application will be accessible at http://localhost:4200 and will communicate to the Node.js application. You should have a fully functional application at this point.
If you don’t want to go through all the steps, you can download the full project here. After downloading and extracting the project, execute the following within the node directory:
npm install
Similarly, navigate into the angular directory and execute the same command. These commands will download any project dependencies for runtime.
You just saw how to build a full stack application using Node.js, NoSQL, and Angular. Using these technologies we can build a backend that communicates with a database and a frontend that communicates with a backend. The great thing about this development approach is that the client facing application can be expanded from Angular and the web into other platforms.
If you’d like to download the full project source code, it can be obtained here.