Back in August I had participated in Midwest JS located inĀ Minneapolis,Ā Minnesota. As you may know, I’m a huge fan of developing full stack applications with the JavaScript stack. This is exactly what I had presented on at the conference.
My session was well attended and many developers were taught how to use Node.js with Couchbase to develop a RESTful API, and Angular as the client facing layer.
As promised, I am going to revisit the material I went over during the presentation so the concepts and code can be reproduced and expanded upon.
Going forward, the assumption is that you’ve got Couchbase Server, Node.js, and the Ionic Framework CLI installed and configured. Couchbase will be the NoSQL Database, Node.js will power our backend, and Ionic Framework will give us a web frontend powered by Angular.
The project created at Midwest JS allowed you to store information about video game consoles and video games for various consoles. This demonstrated the use of CRUD as well as relationships between NoSQL documents and how Couchbase makes it easy.
Creating the Node.js with Couchbase NoSQL Backend
Before we can begin development, we need to create a new Node.js project. From the command line, execute the following:
1 2 |
npm init -y npm install couchbase express body-parser uuid cors --save |
The above command will create a projectĀ package.json file and install our project dependencies. The cors
package will allow us to use Node and Angular locally on two different ports without getting cross origin resource sharing errors. The uuid
package will allow us to generate unique strings for use as document keys. The body-parser
package will allow us to send JSON data in HTTP requests. We’ll be using Express and Couchbase which explains the other two packages.
Create anĀ app.js file within your project. It will contain all the source code for our Node.js application. As boilerplate, it should look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var Couchbase = require("couchbase"); var Express = require("express"); var BodyParser = require("body-parser"); var UUID = require("uuid"); var Cors = require("cors"); var app = Express(); var N1qlQuery = Couchbase.N1qlQuery; app.use(BodyParser.json()); app.use(BodyParser.urlencoded({ extended: true })); app.use(Cors()); var cluster = new Couchbase.Cluster("couchbase://localhost"); var bucket = cluster.openBucket("default", ""); app.get("/consoles", (request, response) => {}); app.post("/console", (request, response) => {}); app.post("/game", (request, response) => {}); app.get("/games", (request, response) => {}); app.get("/game/:id", (request, response) => {}); var server = app.listen(3000, () => { console.log("Listening on port " + server.address().port + "..."); }); |
Notice that we’ve imported each of the downloaded dependencies, initialized and configured Express, and connected to a Bucket in our Couchbase cluster.
We will have five different RESTful API endpoints for this application.
The first logical thing to do would be to create a video game console so we can add games to it. Take a look at the following endpoint code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
app.post("/console", (request, response) => { if(!request.body.title) { return response.status(401).send({ "message": "A `title` is required." }); } var id = UUID.v4(); request.body.type = "console"; bucket.insert(id, request.body, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
In the above logic, we are validating that a title
exists in our request. If it does, we will generate a new id, assign a type
to the data in our request, and insert it into Couchbase. The success or failure response of the insert will be returned to the client, which will eventually be an Angular application.
To query for video game consoles, we’ll need to query based on the type
property. For this reason, we’ll have to use a N1QL query rather than a lookup by id.
1 2 3 4 5 6 7 8 9 10 11 |
app.get("/consoles", (request, response) => { var statement = "SELECT `" + bucket._name + "`.*, META().id FROM `" + bucket._name + "` WHERE type = 'console'"; var query = N1qlQuery.fromString(statement); query.consistency(N1qlQuery.Consistency.REQUEST_PLUS); bucket.query(query, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
The N1QL query is nothing more than a simple SELECT
statement that you’d find in SQL. After executing the query, we would return the response back to the client.
This brings us to the actual video games. Things get a little more complex, but not more difficult.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
app.post("/game", (request, response) => { if(!request.body.title) { return response.status(401).send({ "message": "A `title` is required." }); } else if(!request.body.cid) { return response.status(401).send({ "message": "A `cid` is required." }); } var id = UUID.v4(); request.body.type = "game"; bucket.insert(id, request.body, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
In the above endpoint logic, we plan to insert a new video game into the database. This is no different than what we saw when inserting a new video game console into the database. We are defining a type
property, but we are also making sure a cid
exists. The cid
will be a console id which will allow us to establish a relationship with our data.
When you have relationships, you have JOIN
operations.
1 2 3 4 5 6 7 8 9 10 11 |
app.get("/games", (request, response) => { var statement = "SELECT game.title AS game_title, console.title AS console_title FROM `" + bucket._name + "` AS game JOIN `" + bucket._name + "` AS console ON KEYS game.cid WHERE game.type = 'game'"; var query = N1qlQuery.fromString(statement); query.consistency(N1qlQuery.Consistency.REQUEST_PLUS); bucket.query(query, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
In the above endpoint, we are doing another N1QL query, but this time we have a JOIN
operation. It isn’t useful to return a cid
when querying for video games, so we JOIN
and replace that information with the console title of the other document.
Likewise we have a similar query when trying to find a specific video game:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
app.get("/game/:id", (request, response) => { if(!request.params.id) { return response.status(401).send({ "message": "An `id` is required." }); } var statement = "SELECT game.title AS game_title, console.title AS console_title FROM `" + bucket._name + "` AS game JOIN `" + bucket._name + "` AS console ON KEYS game.cid WHERE game.type = 'game' AND META(game).id = $id"; var query = N1qlQuery.fromString(statement); bucket.query(query, { "id": request.params.id }, (error, result) => { if(error) { return response.status(500).send(error); } response.send(result); }); }); |
The alternative to using N1QL and JOIN
operations would be to do two lookups based on id. There is nothing wrong with this practice, but in my opinion it is easier to just let the database take care of a JOIN
rather than trying to JOIN
via the application layer.
This brings us to the client frontend.
Creating the Ionic Framework with Angular Frontend
As previously mentioned, this time around we are using Ionic Framework which uses a flavor of Angular. I chose this because I was feeling too lazy to create an attractive frontend with Bootstrap or Foundation.
With the Ionic Framework CLI available, execute the following:
1 |
ionic start pwa sidemenu |
The above command will create a project called pwa using the Ionic Framework sidemenu
template.
The base template is useful, but it doesn’t have everything we need. We need to add a few pages to the application.
Using the Ionic Framework generators, or manually, create a consoles, games, and game page. Each of these pages should have an HTML, SCSS, and TypeScript file and each page directory should be in the pages directory.
Open the project’sĀ app/app.component.ts file and make it look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import { Component, ViewChild } from '@angular/core'; import { Nav, Platform } from 'ionic-angular'; import { StatusBar } from '@ionic-native/status-bar'; import { SplashScreen } from '@ionic-native/splash-screen'; import { GamesPage } from '../pages/games/games'; import { ConsolesPage } from '../pages/consoles/consoles'; @Component({ templateUrl: 'app.html' }) export class MyApp { @ViewChild(Nav) nav: Nav; rootPage: any = GamesPage; pages: Array<{title: string, component: any}>; constructor(public platform: Platform, public statusBar: StatusBar, public splashScreen: SplashScreen) { this.initializeApp(); // used for an example of ngFor and navigation this.pages = [ { title: 'Games', component: GamesPage }, { title: 'Consoles', component: ConsolesPage } ]; } initializeApp() { this.platform.ready().then(() => { // Okay, so the platform is ready and our plugins are available. // Here you can do any higher level native things you might need. this.statusBar.styleDefault(); this.splashScreen.hide(); }); } openPage(page) { // Reset the content nav to have just this page // we wouldn't want the back button to show in this scenario this.nav.setRoot(page.component); } } |
Notice that we’ve importedĀ GamesPage
and ConsolesPage
, updated the pages
array, and set the default root page as GamesPage
. By doing this we’ve set up navigation and the default page when the application launches.
To complete the setup, we also need to alter the project’sĀ app/app.module.ts file. Make it look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import { BrowserModule } from '@angular/platform-browser'; import { ErrorHandler, NgModule } from '@angular/core'; import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular'; import { HttpModule } from "@angular/http"; import { MyApp } from './app.component'; import { GamesPage } from '../pages/games/games'; import { GamePage } from '../pages/game/game'; import { ConsolesPage } from '../pages/consoles/consoles'; import { StatusBar } from '@ionic-native/status-bar'; import { SplashScreen } from '@ionic-native/splash-screen'; @NgModule({ declarations: [ MyApp, GamesPage, GamePage, ConsolesPage ], imports: [ BrowserModule, HttpModule, IonicModule.forRoot(MyApp), ], bootstrap: [IonicApp], entryComponents: [ MyApp, GamesPage, GamePage, ConsolesPage ], providers: [ StatusBar, SplashScreen, {provide: ErrorHandler, useClass: IonicErrorHandler} ] }) export class AppModule {} |
Notice that we’ve imported each of our new pages and added them to the declarations
and entryComponents
arrays of the @NgModule
block.
Now we can focus on the development of the application and connecting it to our API.
Open the project’sĀ src/pages/games/games.ts file and make it look like the following. We’re going to break down what is happening next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import { Component } from '@angular/core'; import { NavController, ModalController } from 'ionic-angular'; import { Http, Headers, RequestOptions } from "@angular/http"; import "rxjs/Rx"; import { GamePage } from "../game/game"; @Component({ selector: 'page-games', templateUrl: 'games.html' }) export class GamesPage { public games: Array<any>; public constructor(public navCtrl: NavController, private http: Http, private modalCtrl: ModalController) { this.games = []; } public ionViewDidEnter() { this.http.get("http://localhost:3000/games") .map(result => result.json()) .subscribe(result => { this.games = result; }); } public create() { let gameModal = this.modalCtrl.create(GamePage); gameModal.onDidDismiss(data => { let headers = new Headers({ "Content-Type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("http://localhost:3000/game", JSON.stringify(data), options) .subscribe(result => { this.games.push({ "game_title": data.title, "console_title": ""}); }); }); gameModal.present(); } } |
Within the GamesPage
class, we have a public variable called games
. Because it is public, it will be accessible via the HTML. It will contain all the games returned from the Node.js application.
When the page loads, we want to query our endpoint. It is never a good idea to do this in the constructor
method, so instead we use the ionViewDidEnter
method. After issuing the request, the result is transformed into JSON and then loaded into our public variable.
If we want to create a new game in our database, things are a little different. We are going to display a modal dialog and collect input.
1 2 3 4 5 6 7 8 9 10 11 12 |
public create() { let gameModal = this.modalCtrl.create(GamePage); gameModal.onDidDismiss(data => { let headers = new Headers({ "Content-Type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("http://localhost:3000/game", JSON.stringify(data), options) .subscribe(result => { this.games.push({ "game_title": data.title, "console_title": ""}); }); }); gameModal.present(); } |
The create
method will display our GamePage
which will be in modal format. Any data entered in the form on the modal will be returned back to the GamesPage
and sent via an HTTP request to the API.
Before we take a look atĀ GamePage
, let’s look at the HTML that powers GamesPage
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<ion-header> <ion-navbar> <button ion-button menuToggle> <ion-icon name="menu"></ion-icon> </button> <ion-title>Games</ion-title> <ion-buttons end> <button ion-button icon-only (click)="create()"> <ion-icon name="add"></ion-icon> </button> </ion-buttons> </ion-navbar> </ion-header> <ion-content padding> <ion-list> <button ion-item *ngFor="let game of games"> {{game.game_title}} <span class="item-note">{{game.console_title}}</span> </button> </ion-list> </ion-content> |
In the above HTML, we are looping through our public games
array. Each object in the array is rendered to the screen within a list. Angular does all the heavy lifting for us.
Open the project’sĀ src/pages/game/game.ts file and include the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams, ViewController } from 'ionic-angular'; import { Http, Headers, RequestOptions } from "@angular/http"; import "rxjs/Rx"; @IonicPage() @Component({ selector: 'page-game', templateUrl: 'game.html', }) export class GamePage { public consoles: Array<any>; public input: any; constructor(public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController, private http: Http) { this.consoles = []; this.input = { "cid": "", "title": "" } } ionViewDidEnter() { this.http.get("http://localhost:3000/consoles") .map(result => result.json()) .subscribe(result => { for(let i = 0; i < result.length; i++) { this.consoles.push(result[i]); } }); } public save() { this.viewCtrl.dismiss(this.input); } } |
This modal logic is similar to what we’ve already seen. There will be a form that is bound to HTML and TypeScript. When the ionViewDidEnter
triggers, we query for console information. This console information will eventually be used for a radio list that the user can select from.
When the user selects the save
method, the data bound in the public form is passed to the previous GamesPage
page.
The HTML that powers this modal, found inĀ src/pages/game/game.html looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<ion-header> <ion-navbar> <ion-title>New Game</ion-title> </ion-navbar> </ion-header> <ion-content padding> <ion-list> <ion-item> <ion-label stacked>Title</ion-label> <ion-input type="text" [(ngModel)]="input.title"></ion-input> </ion-item> <ion-item> <ion-label>Console</ion-label> <ion-select [(ngModel)]="input.cid"> <ion-option *ngFor="let console of consoles" value="{{ console.id }}">{{ console.title }}</ion-option> </ion-select> </ion-item> <ion-item> <button ion-button full (click)="save()">Save</button> </ion-item> </ion-list> </ion-content> |
We have a simple list that makes up our form. The form elements are bound to our TypeScript variable and the console information is looped through to populate an HTML select
element.
This brings us to the final page of the Angular frontend.
Open the project’sĀ src/pages/consoles/consoles.ts file and include the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
import { Component } from '@angular/core'; import { NavController, NavParams, AlertController } from 'ionic-angular'; import { Http, Headers, RequestOptions } from "@angular/http"; import "rxjs/Rx"; @Component({ selector: 'page-consoles', templateUrl: 'consoles.html' }) export class ConsolesPage { public consoles: Array<any>; public constructor(public navCtrl: NavController, private http: Http, private alertCtrl: AlertController) { this.consoles = []; } public ionViewDidEnter() { this.http.get("http://localhost:3000/consoles") .map(result => result.json()) .subscribe(result => { this.consoles = result; }); } public create() { let alert = this.alertCtrl.create({ title: 'Add Console', inputs: [ { name: 'title', placeholder: 'Title' }, { name: 'year', placeholder: 'Year' } ], buttons: [ { text: 'Cancel', role: 'cancel', handler: data => { console.log('Cancel clicked'); } }, { text: 'Save', handler: data => { let headers = new Headers({ "Content-Type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("http://localhost:3000/console", JSON.stringify(data), options) .subscribe(result => { this.consoles.push(data); }, error => {}); } } ] }); alert.present(); } } |
While not too different than what we’ve already seen, we have a new feature. We are using a popup dialog to collect input for new video game console information.
When the popup is dismissed, the following is executed:
1 2 3 4 5 6 7 8 |
handler: data => { let headers = new Headers({ "Content-Type": "application/json" }); let options = new RequestOptions({ headers: headers }); this.http.post("http://localhost:3000/console", JSON.stringify(data), options) .subscribe(result => { this.consoles.push(data); }, error => {}); } |
This will take the information found in the form and send it via HTTP to our API which will in turn save the console information to the database.
Awesome right?
The HTML found in the project’sĀ src/pages/consoles/consoles.html file looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<ion-header> <ion-navbar> <button ion-button menuToggle> <ion-icon name="menu"></ion-icon> </button> <ion-title>Consoles</ion-title> <ion-buttons end> <button ion-button icon-only (click)="create()"> <ion-icon name="add"></ion-icon> </button> </ion-buttons> </ion-navbar> </ion-header> <ion-content> <ion-list> <button ion-item *ngFor="let console of consoles"> {{ console.title }} <span class="item-note" item-right> {{ console.year }} </span> </button> </ion-list> </ion-content> |
Again, it is near identical to the other HTML files that we’ve seen.
Conclusion
You just got a recap of everything I went over at Midwest JS 2017. We saw how to create a Node.js API that communicates with Couchbase, our NoSQL database, as well as creating a frontend using Angular and Ionic Framework. These are just a few components of a full stack application.
For more information on using Node.js with Couchbase, check out the Couchbase Developer Portal. If you’d like me to come back to Midwest JS, let me know in the comments.