Jose Navarro is a full stack developer at FAMOCO in Brussels, Belgium. He has been working for the last 3 years as a web developer with Node.js, Java, AngularJS, and ReactJS, and has deep interest in web development and mobile technologies.
Introduction
We are going to develop a REST API using Node.js and Couchbase ODM Ottoman. There are a few frameworks to do this in Node.js, so we are going to use hapi.js, which makes it easy to start and develop an API, and its code is clean and easy to understand. It also provides a validator in the request so we can integrate well with the Ottoman model, which we are going to use so we can abstract our code and work with objects.
Requirements
To build the project, you need to have the following installed on your computer:
-
Node.js and NPM
-
Couchbase Server
Hapi Server
First, we create the main directory for our project, then go inside that directory and start the npm project, where we will be asked for a few parameters for our project.
We can do it with the following commands:
mkdir node–hapi–couchbase–api
npm init
The next step is to add the dependencies to our project. First, we will add the hapi.js related packages, then we add the Couchbase related packages, and finally we add nodemon to our dev dependencies for the live reload of our server while we are coding.
npm install –S hapi joi
npm install –S couchbase ottoman
npm install –D nodemon
Once all this is ready, we start to create our project. We create a folder src where we will have all our code. Inside we create an index.js file where we will have our basic hapi server. There we add the following code:
const Hapi = require(‘hapi’);
// Create a server with a host and port
const server = new Hapi.Server();
server.connection({
host: ‘localhost’,
port: 5000,
routes: {
cors: true,
}
});
// Start the server
server.start( err => {
if( err ) {
// Fancy error handling here
console.error( err );
throw err;
}
console.log( Server started at ${ server.info.uri }
);
} );
module.exports = server;
We have just created our basic server.
Now we are going to define an entry route for our server. First, we create a folder API where we will define our routes. And we create a file index.js, with the code of our entry route:
const routes = [
{
method: ‘GET’,
path: ‘/’,
config: {
handler: (request, reply) => {
return reply({
name: ‘node-hapi-couchbase-api’,
version: 1
});
}
}
}
];
module.exports = routes;
In the main index.js file, we are going to import the routes. For that we add the following code before the server.start code that we defined earlier:
const routes = require(‘./api’);
// Add the routes
server.route(routes);
Now in our package.json file, we will add the script section.
“scripts”: {
“start”: “nodemon ./src/index.js”
},
If we run npm start, we will start our server. We can check it by going to http://localhost:5000, and we should get a response.
{“name”:“node-hapi-couchbase-api”,“version”:1}
Database Connector
To set up the database connector, we are going to create a folder db where we will store the database information and the logic for the connector.
We are going to store the information in the file config.json with the following code:
{
“couchbase”: {
“endpoint”: “localhost:8091”,
“bucket”: “api”
}
}
For the connector, we are going to create a file index.js, where we are going to import the config file and the Couchbase library and initialize the connection with the database and the bucket.
let config = require(‘./config’);
let couchbase = require(‘couchbase’);
let endpoint = config.couchbase.endpoint;
let bucket = config.couchbase.bucket;
let myCluster = new couchbase.Cluster(endpoint, function(err) {
if (err) {
console.log(“Can’t connect to couchbase: %s”, err);
}
console.log(‘connected to db %s’, endpoint);
});
let myBucket = myCluster.openBucket(bucket, function(err) {
if (err) {
console.log(“Can’t connect to bucket: %s”, err);
}
console.log(‘connected to bucket %s’, bucket);
});
The next step is to import the Couchbase ODM Ottoman and set it up with the bucket.
let ottoman = require(‘ottoman’);
ottoman.store = new ottoman.CbStoreAdapter(myBucket, couchbase);
Finally, we are going to export the bucket and Ottoman so we have access from other files.
module.exports = {
bucket: myBucket,
ottoman: ottoman
};
Models
Now that we have our basic server running, we are going to define our models with Ottoman. We are going to define two models: one for a User and another for a Post. For that we create a folder called models, and inside we create two js files: user.js and post.js. We can add the validations in the model, but hapi.js offers a validation before handling the route, so we are going to use that to validate the data that we receive from the user before we pass it to our model.
User Model
The user will have three fields: name, email, and password. We create our user model using the package Ottoman. Our user model contains the following code:
let ottoman = require(‘../db’).ottoman;
let UserModel = ottoman.model(‘User’, {
password: ‘string’,
name: ‘string’,
email: ‘string’,
}, {
index: {
findByEmail: {
by: ’email’,
type: ‘refdoc’
}
}
});
First, we import the Ottoman instance that we initiated in the db connector. After that we start defining our model. The first parameter is the name of our model, in this case ‘User’. The second parameter is the JSON object that contains the name of the field and the type; in our case all the values are string (check Ottoman documentation to see other types). The next parameter is the object that contains the index we want to create. We are going to create an index for the email so we can use that index to query for the user using our model; this will also create a restriction to avoid duplicated emails in our users.
When we create an index we need to call the function ensureIndices to create the indexes internally.
ottoman.ensureIndices(function(err) {
if (err) {
return console.error(‘Error ensure indices USER’, err);
}
console.log(‘Ensure indices USER’);
});
The last step is to export the model.
module.exports = UserModel;
Post Model
The post will contain four fields: title and body, the timestamp, and the user.
First, we import the Ottoman instance that we initialized in the db connector, and we also import the User model.
let ottoman = require(‘../db’).ottoman;
let User = require(‘./user’);
let PostModel = ottoman.model(‘Post’, {
user: User,
title: ‘string’,
body: ‘string’,
timestamp: {
type: ‘Date’,
default: Date.now
}
});
The first parameter is the name of our model, ‘Post’. The second is the JSON object with our field. In this case we define user with the type User that we defined in our previous model; the title and body of type string, and timestamp of type Date. We are going to create a default value with the current timestamp when the object is created.
And finally we export our model.
module.exports = PostModel;
API Routes
We are going to define our routes for Users and Posts; the basic path we are going to use is /api/v1. In our index.js file inside API, we are going to import the user routes and the post routes, and we will join them in an array.
const users = require(‘./users’);
const posts = require(‘./posts’);
…
routes = routes.concat(users);
routes = routes.concat(posts);
In both User and Post routes, we are going to define the methods to perform a CRUD operation. For every route, we need to define the method, path, and config. In the config section we provide the handler, that it is the function to perform; and we can also provide a validate function that will be called before we perform the handle function. For validations, we are going to use the Joi package, which we can use to define the schema and validations for the body of the request.
User routes
For users, we are going to use the path /api/v1/users. The first step in our routes file is to import the User model and the joi package.
const User = require(‘../models/user’);
const Joi = require(‘joi’);
Retrieve the list of users GET /api/v1/users
In the handle function we are going to use the find function from the User model that allows us to query the db to collect all the documents of type User.
{
method: ‘GET’,
path: ‘/api/v1/users’,
config: {
handler: (request, reply) => {
User.find({}, (err, users) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
return reply({
data: users,
count: users.length
});
});
}
}
}
We are going to return an object with an array of user and a count with the number of objects inside the array.
Retrieve a User by its id GET /api/v1/users/{id}
In this case, we are going to query for a user by its document id, so we are going to use the built-in function getById in our model to retrieve a document from the database.
In this case, we provide a validate object to validate that the param value id is a string.
{
method: ‘GET’,
path: ‘/api/v1/users/{id}’,
config: {
handler: (request, reply) => {
User.getById(request.params.id, (err, user) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
return reply(user);
});
},
validate: {
params: {
id: Joi.string(),
}
}
}
}
We are going to return the document of the user.
Create a new user POST /api/v1/users
Now we are going to create a new user. The first step is to create the User with the User model and the body of the request.
We provide a validate object to check that payload (body of the request).
{
method: ‘POST’,
path: ‘/api/v1/users’,
config: {
handler: (request, reply) => {
const user = new User(request.payload);
user.save((err) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
return reply(user).code(201);
});
},
validate: {
payload: {
password: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
name: Joi.string()
}
}
}
}
We will return the object of the new user created.
Update a user PUT /api/v1/users/{id}
Now we are going to update a user. In this case we are first going to retrieve the user document from the database, then we will update the fields, and finally we will save the updated document in the database.
In this case, we provide a validate object where we validate both params and payload.
{
method: ‘PUT’,
path: ‘/api/v1/users/{id}’,
config: {
handler: (request, reply) => {
User.getById(request.params.id, (err, user) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
const payload = request.payload;
if (payload.name) {
user.name = payload.name;
}
if (payload.password) {
user.password = payload.password;
}
user.save((err) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
return reply(user).code(200);
});
});
},
validate: {
params: {
id: Joi.string(),
},
payload: {
name: Joi.string(),
password: Joi.string().alphanum().min(3).max(30),
}
}
}
}
We will return the updated document.
Delete a user DELETE /api/v1/users/{id}
In this case we are going to delete a user. First we retrieve the document from the database, and then we remove it.
{
method: ‘DELETE’,
path: ‘/api/v1/users/{id}’,
config: {
handler: (request, reply) => {
User.getById(request.params.id, (err, user) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
user.remove((err) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
return reply(user);
});
});
},
validate: {
params: {
id: Joi.string(),
}
}
}
}
We will return the document deleted.
Finally, we need to export the routes.
module.exports = routes;
Post routes
For the post routes we are going to use the path /api/v1/users/{userId}/posts, so we only perform operations to post related to the user. We will define a validation function that it is going to check if the user exists in the database, and return it so we have access to the user in the function that handles the request.
The first section of the code is the imports and that function.
const User = require(‘../models/user’);
const Post = require(‘../models/post’);
const Joi = require(‘joi’);
const validateUser = (value, options, next) => {
const userId = options.context.params.userId;
User.getById(userId, (err, user) => {
next(err, Object.assign({}, value, { user }))
})
};
Retrieve the list of posts of the user GET /api/v1/users/{userId}/posts
To retrieve all the user’s posts we are going to use the Post model and the function find. We are going to execute with an object where we are going to provide the id of the user to retrieve all the Post of the user.
{
method: ‘GET’,
path: ‘/api/v1/users/{userId}/posts’,
config: {
handler: (request, reply) => {
const user = request.query.user;
Post.find({ user: { _id: user._id } }, (err, posts) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
return reply({
data: posts,
count: posts.length
});
})
},
validate: {
query: validateUser,
}
}
}
We are going to return an object with an array of post and the count of posts.
Retrieve a post GET /api/v1/users/{userId}/posts/{postId}
Like we do in the list of post, to retrieve one post we are going to call the function find with the user id, and also with the post id that we want to retrieve.
{
method: ‘GET’,
path: ‘/api/v1/users/{userId}/posts/{postId}’,
config: {
handler: (request, reply) => {
const user = request.query.user;
const postId = request.params.postId;
Post.find({ user: { _id: user._id }, _id: postId }, (err, posts) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
if (posts.length === 0) {
return reply({
status: 404,
message: ‘Not Found’
}).code(404);
} else {
return reply(posts[0]);
}
})
},
validate: {
query: validateUser,
}
}
}
We are going to return the first post that we receive. We can only receive one post because we are querying the db to find a post by its id, which is why we return the first item of the array. If we do not get any post, it means there is no post with that id related to that user, so we return a not found error.
Create a new post POST /api/v1/users/{userId}/posts
To create a post, we are going to do the same process as we did in the user. We provide a validate object for the payload so we can validate the body that we receive. We only validate the title and the body of the post because the user we are getting it from — the path and the timestamp — is generated when we create the post.
In the handler function we create a new post with the payload, and we set the user of the post with the user.
{
method: ‘POST’,
path: ‘/api/v1/users/{userId}/posts’,
config: {
handler: (request, reply) => {
const user = request.query.user;
const post = new Post(request.payload);
post.user = user;
post.save((err) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
return reply(post).code(201);
});
},
validate: {
query: validateUser,
payload: {
title: Joi.string().required(),
body: Joi.string().required(),
}
}
}
}
We will return the post created.
Update a post PUT /api/v1/users/{userId}/posts/{postId}
To update a post, we provide a validate object like we did in the create, and we are going to allow change to the title and the body of the post. Here we are going to query the post using the Post model and getById function, so when we retrieve the post, we check if the user matches the user provided in the path. If it matches, we update the fields in the post with the values of the request, and we save the updated post.
{
method: ‘PUT’,
path: ‘/api/v1/users/{userId}/posts/{postId}’,
config: {
handler: (request, reply) => {
Post.getById(request.params.postId, (err, post) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
if (request.params.userId === post.user._id) {
const payload = request.payload;
if (payload.title) {
post.title = payload.title;
}
if (payload.body) {
post.body = payload.body;
}
post.save((err) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
return reply(post).code(200);
});
} else {
return reply({
status: 401,
message: “The user can not edit the post”
}).code(401);
}
})
},
validate: {
query: validateUser,
payload: {
title: Joi.string().required(),
body: Joi.string().required(),
}
}
}
}
We will return the updated post. If the user does not match the user in the post, we receive an authorized error because the user is not the owner of the post.
Delete a post DELETE /api/v1/users/{userId}/posts/{postId}
As we did in the update, we query for the post and we check if the user of the path matches the user of the post. If they match, we proceed and delete the post.
{
method: ‘DELETE’,
path: ‘/api/v1/users/{userId}/posts/{postId}’,
config: {
handler: (request, reply) => {
Post.getById(request.params.postId, (err, post) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
if (request.params.userId === post.user._id) {
post.remove((err) => {
if (err) {
return reply({
status: 400,
message: err.message
}).code(400);
}
return reply(post).code(200);
});
} else {
return reply({
status: 401,
message: “The user can not delete the post”
}).code(401);
}
})
},
validate: {
query: validateUser
}
}
}
We return the deleted post.
Finally we export the routes.
module.exports = routes;
Test
To test the API, we can do it with Postman, cURL, or any other application.
Below we created some cURL examples to test the API. The ids used are the ones that we created with the POST operations, so when you execute them, remember to change the path to match the ids of the resources you have generated.
# we query for the users
curl –X GET “http://localhost:5000/api/v1/users”
# we create a user
curl –X POST –H “Content-Type: application/json” –d ‘{
“name”: “jose”,
“password”: “jose”,
“email”: “jose.navarro@famoco.com”
}’ “http://localhost:5000/api/v1/users”
# we should get a json with a user
curl –X GET “http://localhost:5000/api/v1/users”
# we should get the users with that id
curl –X GET “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# we update the user
curl –X PUT –H “Content-Type: application/json” –d ‘{
“name”: “jose_update”,
“password”: “joseedit”
}’ “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# we delete the user
curl –X DELETE “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# posts
# we query for the post of a user
curl –X GET “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts”
# we create a post
curl –X POST –H “Content-Type: application/json” –d ‘{
“title”: “my post title”,
“body”: “my post body”
}’ “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts”
# we query for a post
curl –X GET “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
# we update a post
curl –X PUT –H “Content-Type: application/json” –d ‘{
“title”: “my edited title”,
“body”: “my edited body”
}’ “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
# we delete a post
curl –X DELETE “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
Conclusion
As we have seen, it was easy to develop a basic REST API to perform a CRUD operation, and the code is simple and easy to read. And with Ottoman we were able to abstract the db logic to work with object and the methods the ODM provided us.