Previously I wrote about how to create an AngularJS application with PouchDB and Couchbase. The way I demonstrated is of course one way to accomplish the job of using Couchbase in a web application. This time around we’re going to take a look at creating a full stack application using the Couchbase, Express.js, AngularJS, and Node.js (CEAN) stack.
You might be familiar with the MongoDB, Express.js, AngularJS, and Node.js (MEAN) stack, but I’m going to show you that this is just as possible with Couchbase and even easier to do.
We’re going to see how to install and configure all four of the CEAN stack services as well as make a very simple application out of them.
The whole setup process doesn’t take long so we’re going to run through it real quick.
Let’s start with installing our database, Couchbase Server. Head over to the downloads section of the Couchbase website and get the latest installation for your computer, whether it be Mac, Windows, or Linux.
Install the file that you downloaded and launch Couchbase Server. Upon first launch it should take you through a configuration wizard. The wizard takes about two minutes to complete.
Couchbase stores all NoSQL documents in what’s called a bucket. We’ll need to create one called restful-sample in order to continue in the application. From the Data Buckets tab of your Couchbase administrative dashboard, choose to Create New Data Bucket and name it appropriately.
With the bucket created, we need to create a primary index on it. This will allow us to run N1QL queries against the data. You’ll see what they are and why they are beautiful deeper in this tutorial.
Run the Couchbase Query Client (CBQ) from the Terminal or Command Prompt. On a Mac, it is found at the following path:
./Applications/Couchbase Server.app/Contents/Resources/couchbase-core/bin/cbq
On a Windows computer it is found at the following path:
C:/Program Files/Couchbase/Server/bin/cbq.exe
With CBQ open, run the following statement to create a primary index:
CREATE PRIMARY INDEX ON `restful-sample` USING GSI;
Our Couchbase Bucket is now ready to go!
Now we’re going to install Node.js. Although we won’t see it in this step, the Node Package Manager (NPM) included with Node.js will be used to download Express.js and its dependencies. Head on over to the Node.js website and download whatever version is appropriate for your operating system.
Install Node.js however you typically would based on your operating system.
Create a new directory, maybe on your desktop, called CEAN. Using the Terminal (Mac and Linux) or Command Prompt (Windows), run the following with CEAN as your current working directory:
npm init
Answer all the questions to the best of your ability, or just create a file called package.json with the following contents:
{
"name": "cean-project",
"version": "1.0.0",
"description": "An example of using the Node.js SDK for Couchbase to create a simple CEAN stack",
"author": "Couchbase, Inc.",
"license": "MIT"
}
Since we have NPM working through the Node.js installation, we need to install Express.js and a few other dependencies locally to our project. From the Terminal or Command Prompt, run the following command:
npm install body-parser couchbase express uuid --save
Let’s break down what we just installed. The body-parser dependency will allow us to make POST requests to our backend API. The couchbase dependency will allow you to communicate from application server to Couchbase Server. The express dependency of course allows us to use the Express.js framework and the uuid dependency will allow us to generate unique ids later in our project.
Before we bring setup to a close, lets define our project structure. Go ahead and create the following files and directories:
CEAN
routes
routes.js
models
recordmodel.js
public
js
app.js
css
templates
item.html
list.html
index.html
app.js
package.json
config.json
We’ll be adding other libraries and style sheets, but above is everything we’ll be creating.
The back-end will be responsible for serving data to our front-end (AngularJS). It is endpoint based making separation between front-end and back-end easier.
All our server configuration will be in the app.js file found at the root of our project. However, to keep our project clean, we’re going to put all our constant variables in their own file. This file will be called config.json and it will contain the following:
{
"couchbase": {
"server": "127.0.0.1:8091",
"bucket": "restful-sample"
}
}
We essentially only have two constants for this project. What server we plan to use and what bucket we want to use. A bucket in Couchbase terms is a collection of NoSQL documents.
Open app.js at the root of your project and include the following code:
var express = require("express");
var bodyParser = require("body-parser");
var couchbase = require("couchbase");
var path = require("path");
var config = require("./config");
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
module.exports.bucket = (new couchbase.Cluster(config.couchbase.server)).openBucket(config.couchbase.bucket);
app.use(express.static(path.join(__dirname, "public")));
var routes = require("./routes/routes.js")(app);
var server = app.listen(3000, function () {
console.log("Listening on port %s...", server.address().port);
});
Let’s break down what is happening here. First off we are requiring all the dependencies that we downloaded with NPM into our project. We are then telling the application server that we want to accept POST requests with JSON bodies as well as URL encoded data.
module.exports.bucket = (new couchbase.Cluster(config.couchbase.server)).openBucket(config.couchbase.bucket);
Here we connect to the Couchbase cluster and open a particular bucket, both defined in our config.json file. This opened bucket is assigned to a global variable through the module.exports
command.
app.use(express.static(path.join(__dirname, "public")));
The above line will allow us to place HTML, JavaScript and other front-end files in our project. The front-end and back-end will still communicate over requests, but the front-end code is still going to be bundled with our project.
Finally we are including our routes/routes.js file which will contain paths to each of our endpoints and the server listener.
Before we set up our routes, let’s determine some functions for communicating with the database. All of these functions will go into the RecordModel
class found in the models/recordmodel.js file. These functions will be responsible for inserting, updating, retrieving, and deleting data from Couchbase. Pretty much your CRUD basics.
Let’s start by laying the foundation to this models/recordmodel.js file. Open it and add the following:
var uuid = require("uuid");
var db = require("../app").bucket;
var config = require("../config");
var N1qlQuery = require('couchbase').N1qlQuery;
function RecordModel() { };
module.exports = RecordModel;
You can see above that we are requiring the uuid dependency that we downloaded as well as the app.js and config.json files from the root of our project. This is because we plan to use all these.
Now let’s break down all the functions starting with the save
function. In your models/recordmodel.js file, add the following function before the module.exports = RecordModel;
line:
RecordModel.save = function(data, callback) {
var jsonObject = {
firstname: data.firstname,
lastname: data.lastname,
email: data.email
}
var documentId = data.document_id ? data.document_id : uuid.v4();
db.upsert(documentId, jsonObject, function(error, result) {
if(error) {
callback(error, null);
return;
}
callback(null, {message: "success", data: result});
});
}
What is going on here? Well we are accepting an object of data and a callback passed in from the soon to be created parent route. We are extracting only the data elements we need from the data
variable and we are constructing a document key to be used. This save function will handle both updates and inserts (upsert) so keys are very important. Basically, if a document id was passed from the route, use it, otherwise create some unique value and use that. Upsert will decide whether or not we should insert or update.
Depending on the how the upsert
function responds is how we make use of the parent callback function.
With save
out of the way, lets take a look at the getByDocumentId
function for fetching a specific document. In your models/recordmodel.js file, add the following:
RecordModel.getByDocumentId = function(documentId, callback) {
var statement = "SELECT firstname, lastname, email " +
"FROM `" + config.couchbase.bucket + "` AS users " +
"WHERE META(users).id = $1";
var query = N1qlQuery.fromString(statement);
db.query(query, [documentId], function(error, result) {
if(error) {
return callback(error, null);
}
callback(null, result);
});
};
This function again is accepting two parameters from the parent endpoint, a document id and a callback. However, this time we are using a very SQL-like statement referred to by Couchbase as N1QL. It is a parameterized query to prevent injection attacks, but it essentially just finds any NoSQL document where the document key matches the value passed in the function.
The next function, delete
, is pretty simple. Inside the models/recordmodel.js file add the following:
RecordModel.delete = function(documentId, callback) {
db.remove(documentId, function(error, result) {
if(error) {
callback(error, null);
return;
}
callback(null, {message: "success", data: result});
});
};
We’re just passing a document id and a callback and making use of the Node.js SDK for Couchbase. Very simple and nothing special.
Finally our last function, getAll
, which will get all documents from our Couchbase bucket:
RecordModel.getAll = function(callback) {
var statement = "SELECT META(users).id, firstname, lastname, email " +
"FROM `" + config.couchbase.bucket + "` AS users";
var query = N1qlQuery.fromString(statement).consistency(N1qlQuery.Consistency.REQUEST_PLUS);
db.query(query, function(error, result) {
if(error) {
return callback(error, null);
}
callback(null, result);
});
};
Again, we’re using N1QL here because it is very convenient for getting a bunch of documents back.
Wew! We now have all the logic in our CEAN application that interfaces with Couchbase Server. This leaves us to designing our endpoints and font-end.
Everything we do here will be in the routes/routes.js file. The foundation to this file will look like the following:
var RecordModel = require("../models/recordmodel");
var appRouter = function(app) {
};
module.exports = appRouter;
Every route will end up inside of the appRouter
function. Let’s go in the same order we did for the class functions, starting with the save
route:
app.post("/api/save", function(req, res) {
if(!req.body.firstname) {
return res.status(400).send({"status": "error", "message": "A firstname is required"});
} else if(!req.body.lastname) {
return res.status(400).send({"status": "error", "message": "A lastname is required"});
} else if(!req.body.email) {
return res.status(400).send({"status": "error", "message": "A email is required"});
}
RecordModel.save(req.body, function(error, result) {
if(error) {
return res.status(400).send(error);
}
res.send(result);
});
});
When the user (or front-end) hits the /api/save endpoint with a POST request, the above will happen. We first make sure the POST body contains a firstname
, lastname
, and email
, otherwise we return an error. If those conditions are met then we call the save
function of our RecordModel
class. The callback is passed and depending on what the child function returns, is what we return back to the user (or front-end).
With save out of the way, let’s look at the get
endpoint for getting a particular document from the database:
app.get("/api/get", function(req, res) {
if(!req.query.document_id) {
return res.status(400).send({"status": "error", "message": "A document id is required"});
}
RecordModel.getByDocumentId(req.query.document_id, function(error, result) {
if(error) {
return res.status(400).send(error);
}
res.send(result);
});
});
When the front-end hits the /api/get endpoint with a GET request, the above will happen. We first make sure the GET query contains a document_id
and if that condition is met then we can proceed to calling the getByDocumentId
function of our RecordModel
class.
I’m sure you can see a trend here in our endpoint design.
Moving on, we want to make an endpoint for deleting data. In the routes/routes.js file add the following function:
app.post("/api/delete", function(req, res) {
if(!req.body.document_id) {
return res.status(400).send({"status": "error", "message": "A document id is required"});
}
RecordModel.delete(req.body.document_id, function(error, result) {
if(error) {
return res.status(400).send(error);
}
res.send(result);
});
});
If a POST request to the /api/delete endpoint is made above, we first confirm that the document_id
exists in the POST body. If it does, then call the delete
function of the RecordModel
class.
Finally we have our last endpoint. The last endpoint is responsible for returning all documents that exist in the database. It can be seen in the following code:
app.get("/api/getAll", function(req, res) {
RecordModel.getAll(function(error, result) {
if(error) {
return res.status(400).send(error);
}
res.send(result);
});
});
There are no requirements above. As long as a GET request is made to the /api/getAll endpoint, data will be returned.
Our back-end is done. We can celebrate now! Not exactly, we still need a front-end to communicate to it. Any front-end will work, but in this CEAN stack we’re going to be using AngularJS.
The front-end will be responsible for requesting data from our back-end (Node.js and Couchbase Server). It makes HTTP requests to the back-end allowing for better separation and flexibility between the front-end and back-end.
The front-end we’re making is of course using AngularJS, but it is also using the AngularJS UI-Router library as well as Twitter Bootstrap. The first thing we want to do is download everything.
After downloading everything, place the minified (.min.js) files in your project’s public/js directory. Place all CSS files in your project’s public/css directory, and place all fonts in your project’s public/fonts directory.
Now everything can be included in your projects public/index.html file. Open it and include the following:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/bootstrap.min.css">
<script src="js/angular.min.js"></script>
<script src="js/angular-ui-router.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/app.js"></script>
</head>
<body ng-app="recordsapp">
<div style="padding: 20px">
<div ui-view></div>
</div>
</body>
</html>
You may be wondering what ng-app="recordsapp"
is or what <div ui-view></div>
is. Don’t worry, that is coming soon.
Inside your public/js directory you should have an app.js file. Go ahead and add the following foundation code:
angular.module("recordsapp", ["ui.router"])
.config(function($stateProvider, $urlRouterProvider) {
})
.controller("MainController", function($scope, $http, $state, $stateParams) {
});
You can see here that we’ve named our module recordsapp
which relates to what we saw in the public/index.html file. We’ve also injected the ui.router
and built functions for our .config
and .controller
.
If you’re unfamiliar with the AngularJS UI-Router, it is a wonderful library for adding multiple screens (views) to your front-end application. All configuration for these views will end up in our config
function in the public/js/app.js file:
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state("list", {
"url": "/list",
"templateUrl": "templates/list.html",
"controller": "MainController",
"cache": false
})
.state("item", {
"url": "/item/:documentId",
"templateUrl": "templates/item.html",
"controller": "MainController",
"cache": false
});
$urlRouterProvider.otherwise("list");
})
Essentially we’re creating two views. We are creating a list view and a view for adding or editing items of the list. Each of the routes points to a particular controller and template file. Both routes point to the MainController
that we’ll see soon, but each point to either a public/templates/list.html file or public/templates/item.html file.
By default, the list view will show through the $urlRouterProvider.otherwise("list");
line.
We know which template files we need to work on now as explained by the AngularJS UI-Router configuration. Starting with the public/templates/list.html file, add the following code:
<button class="btn btn-primary" ui-sref="item">New Item</button>
<br />
<table class="table table-striped" ng-init="fetchAll()">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(key, value) in items">
<td>{{value.firstname}}</td>
<td>{{value.lastname}}</td>
<td>{{value.email}}</td>
<td><a href="" ui-sref="item({documentId: key})">edit</a> | <a href="" ng-click="delete(key)">delete</a></td>
</tr>
</tbody>
</table>
Essentially we have a table that loops through each object within a particular object to populate the table. Functions like fetchAll
and delete
are still unknown at this point, but they are coming. If you take a look at the ui-sref
items in the code you’ll notice they are referring to particular UI-Router states. One of which passes in an optional parameter containing the document key. You’ll see how it’s used soon.
The second template we want to look at is the public/templates/item.html file:
<form>
<div class="form-group">
<label for="firstname">First Name</label>
<input type="text" class="form-control" id="firstname" placeholder="First Name" ng-model="inputForm.firstname">
</div>
<div class="form-group">
<label for="lastname">Last Name</label>
<input type="text" class="form-control" id="lastname" placeholder="Last Name" ng-model="inputForm.lastname">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="text" class="form-control" id="email" placeholder="Email" ng-model="inputForm.email">
</div>
<button type="button" class="btn btn-danger" ui-sref="list">Cancel</button>
<button type="button" class="btn btn-success" ng-click="save(inputForm.firstname, inputForm.lastname, inputForm.email)">Save</button>
</form>
This file is nothing more than a form. When the success button is pressed, a save
function is called. We haven’t created it yet, but it is coming.
The last part of our front-end is all of the application logic to fuel the visuals. Inside your project’s public/js/app.js file, add the following to your controller
:
.controller("MainController", function($scope, $http, $state, $stateParams) {
$scope.items = {};
$scope.fetchAll = function() {
$http(
{
method: "GET",
url: "/api/getAll"
}
)
.success(function(result) {
for(var i = 0; i < result.length; i++) {
$scope.items[result[i].id] = result[i];
}
})
.error(function(error) {
console.log(JSON.stringify(error));
});
}
if($stateParams.documentId) {
$http(
{
method: "GET",
url: "/api/get",
params: {
document_id: $stateParams.documentId
}
}
)
.success(function(result) {
$scope.inputForm = result[0];
})
.error(function(error) {
console.log(JSON.stringify(error));
});
}
$scope.delete = function(documentId) {
$http(
{
method: "POST",
url: "/api/delete",
data: {
document_id: documentId
}
}
)
.success(function(result) {
delete $scope.items[documentId];
})
.error(function(error) {
console.log(JSON.stringify(error));
});
}
$scope.save = function(firstname, lastname, email) {
$http(
{
method: "POST",
url: "/api/save",
data: {
firstname: firstname,
lastname: lastname,
email: email,
document_id: $stateParams.documentId
}
}
)
.success(function(result) {
$state.go("list");
})
.error(function(error) {
console.log(JSON.stringify(error));
});
}
});
That is a lot of $http
requests, and that’s the point! The front-end of this stack is only suppose to request data from the back-end. It makes it very modular.
At this point your Couchbase Server should be up and running. From the Command Prompt or Terminal, run the following with the project as your current working directory:
node app.js
If you visit http://localhost:3000 from your web browser you should have a fully functional CEAN stack application.
Above is what your application should look like.
You just saw how to build a full stack CEAN stack application. Our code was short, clean, and very easy to understand thanks to the beauty of the Node.js SDK for Couchbase.
If you’re interested in the full source code to this project, it can be downloaded from the Couchbase Labs GitHub repository.