Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Creating a User Profile Store With Node.js and a NoSQL Database

DZone's Guide to

Creating a User Profile Store With Node.js and a NoSQL Database

Dive deeper into designing user profiles store with Couchbase and see how to create a simple user profile store using Node.js and Couchbase Server.

· Database Zone
Free Resource

Traditional relational databases weren’t designed for today’s customers. Learn about the world’s first NoSQL Engagement Database purpose-built for the new era of customer experience.

There are many use cases for NoSQL databases, but one that I encounter frequently is in the realm of storing user data. A user profile store is great for NoSQL because profiles often need to be flexible and able to accept data changes at any given time. While possible in an RDBMS, it would potentially require more work in maintaining the data with a penalty on performance.

About a year ago, Kirk Kirkconnell had written a high-level example of designing a user profile store with Couchbase titled User Profile Store: Advanced Data Modeling.

We’re going to expand on these ideas and see how to create a simple user profile store using Node.js and Couchbase Server.

Before we see some code, let’s figure out what we’re trying to accomplish.

When it comes to managing user data, we know that we need a way to create profiles and associate things to those profiles. We need to extend this idea so it follows a better practice. When designing our profile store, we shouldn’t store account data such as username and password in the profile itself. We also shouldn’t pass sensitive user data with every request for a user action. We should instead use a session that expires.

For these reasons, we want the following API endpoints:

  • POST /account: Create a new user profile with account information.
  • POST /login: Validate account information.
  • GET /account: Get account information.
  • POST /blog: Create a new blog entry associated with a user.
  • GET /blogs: Get all blog entries for a particular user.

With this information in mind, we can start developing our backend for the user profile store. The assumption is that you’ve already gotten Couchbase Server and Node.js installed and configured.

Creating a Node.js With Express Framework Application

The first order of business is to create a new Node.js application with all the required dependencies. Using the Node Package Manager (NPM), execute the following:

npm init --y
npm install couchbase express body-parser uuid bcryptjs --save

The above commands will initialize a new project and install the dependencies. We’ll be using the Node.js Couchbase SDK with Express Framework. To accept JSON data via POST requests, we’ll need the body-parser package. The uuidpackage will allow us to generate unique keys and bcryptjs will allow us to hash our passwords to deter malicious users.

Let’s bootstrap our application now.

Create an app.js file within the project that contains the following JavaScript code:

var Couchbase = require("couchbase");
var Express = require("express");
var UUID = require("uuid");
var BodyParser = require("body-parser");
var Bcrypt = require("bcryptjs");

var app = Express();
var N1qlQuery = Couchbase.N1qlQuery;

app.use(BodyParser.json());
app.use(BodyParser.urlencoded({ extended: true }));

var cluster = new Couchbase.Cluster("couchbase://localhost");
var bucket = cluster.openBucket("default", "");

var server = app.listen(3000, () => {
    console.log("Listening on port " + server.address().port + "...");
});

The above code includes the downloaded dependencies and then initializes them within the project. Then we are telling the application to connect to a locally running Couchbase instance and open the default Bucket.

Because we’ll be using N1QL, we’ll need an index created within our Bucket. You can create a primary index, but that is never a good idea for production. Instead, create the following index:

CREATE INDEX `blogbyuser` ON `default`(type, pid);

The only N1QL query we plan to use will obtain all blog posts for a particular profile ID. We’ll get much better performance using a specific index rather than a primary index.

The API that we create with Node.js will be serving endpoints on port 3000.

Saving New Users to the Profile Store

As of right now, we don’t have any users in our profile store. Creating an endpoint for accomplishing this job should be our first goal. Before we create the endpoint, let’s figure out what we want to do.

We know that a user profile can have any information describing a user, whether that be addresses, phone numbers, social media information, or something else. It is never a good idea to store account credentials with the profile information. For this reason, we’ll have a minimum of two documents for every user.

The profile document might be modeled like the following:

{
    "email": "nic@example.com",
    "firstname": "Nic",
    "lastname": "Raboy",
    "social_media": {
        "twitter": "nraboy"
        "website": "https://www.thepolyglotdeveloper.com"
    },
    "type": "profile"
}

In the above, notice that the type is an important indicator which tells us what kind of document this is. The account credentials associated with the profile might be modeled like the following:

{
    "type": "account",
    "pid": "a208dca4-601f-4fd1-97d1-89964716931f",
    "email": "nic@example.com",
    "password": "$2a$10$ABRexSV9EdB67JqEqI.QmO3RC6v2lgTEMan78HW4JP6tvsuMqqjMq"
}

The above document also has a type, but it is describing an account, not a profile. A very important indicator here is the pid property. This property should match a particular profile’s id. Essentially we are establishing a document relationship without any database constraints.

So let’s code this.

In the app.js file, include the following JavaScript code:

app.post("/account", (request, response) => {
    if(!request.body.email) {
        return response.status(401).send({ "message": "An `email` is required" });
    } else if(!request.body.password) {
        return response.status(401).send({ "message": "A `password` is required" });
    }
    var id = UUID.v4();
    var account = {
        "type": "account",
        "pid": id,
        "email": request.body.email,
        "password": Bcrypt.hashSync(request.body.password, 10)
    };
    var profile = request.body;
    profile.type = "profile";
    delete profile.password;
    bucket.insert(id, profile, (error, result) => {
        if(error) {
            return response.status(500).send(error);
        }
        bucket.insert(request.body.email, account, (error, result) => {
            if(error) {
                bucket.remove(id);
                return response.status(500).send(error);
            }
            response.send(result);
        });
    });
});

There is quite a bit happening in the above endpoint so we’ll break it down.

First, we are checking to make sure both an email and a password exists in the request. Chances are, you’ll want to validate more than just those properties, but for simplicity of the demo, those two are fine.

Now, we create an account object and profile object based on the data that was sent in the request. The pid that we’re saving into the account object is a unique key. It will also be set as the document key for our profile object. However, the account document will have the email address as the key. In the future, if other account methods are added (alternate email, social login, etc.), we can always associate more documents to a profile.

Rather than saving the password into the account object as plain text, we are hashing it with Bcrypt. For more information on password hashing with Node.js and Couchbase, check out a previous tutorial I wrote on the subject. The password is stripped from the profile object for security.

With the data ready, we can insert it into Couchbase. The goal of the save is to be all or nothing. In other words, we want both the account and profile documents to be created successfully, and otherwise roll something back. Depending on the success, return something back to the client.

We could have used N1QL queries for inserting the data, but it is significantly easier to do a CRUD operation in this scenario with no penalty to performance.

Exchanging Sensitive Information With a Session Token

With the user profile and account created, we want the user to be able to sign in and start doing activities that will store data and associate it with them. Again, it is never a good idea to pass the username and password around with every request.

For this reason, we want to log in and establish a session with the user. The session will be stored in the database, it will reference a particular user profile, and it will expire and be automatically removed from the database.

The session model might look like the following:

{
    "type": "session",
    "id": "0415353a-ef8f-41e1-9b56-2adacaf88cb7",
    "pid": "a208dca4-601f-4fd1-97d1-89964716931f"
}

This document, like the others, has a different type. Just like with the account document, it has a pid that references a profile.

The code that makes this possible might look like the following:

app.post("/login", (request, response) => {
    if(!request.body.email) {
        return response.status(401).send({ "message": "An `email` is required" });
    } else if(!request.body.password) {
        return response.status(401).send({ "message": "A `password` is required" });
    }
    bucket.get(request.body.email, (error, result) => {
        if(error) {
            return response.status(500).send(error);
        }
        if(!Bcrypt.compareSync(request.body.password, result.value.password)) {
            return response.status(500).send({ "message": "The password is invalid" });
        }
        var session = {
            "type": "session",
            "id": UUID.v4(),
            "pid": result.value.pid
        };
        bucket.insert(session.id, session, { "expiry": 3600 }, (error, result) => {
            if(error) {
                return response.status(500).send(error);
            }
            response.send({ "sid": session.id });
        });
    });
});

After validating the incoming data we do an account lookup by the email address. If data comes back for the email, we can compare the incoming password with the hashed password returned in the account lookup. Provided that succeeds, we can start creating a new session for the user.

Unlike the previous insert operation, we are setting a document expiration of an hour. After an hour, if the expiration hasn’t been refreshed, the document will disappear from the database. This is good because it will force the user to sign in again and get a new session. This session token will be passed with every future request instead of the password.

Managing a Particular User With the Session Token

At this point, we want to be able to get information about our user profile as well as associate new things to the profile. For this, we’ll need to confirm we have authority through the session.

We can confirm the session is valid through a simple Node.js middleware. It might look something like the following:

var validate = function(request, response, next) {
    var authHeader = request.headers["authorization"];
    if(authHeader) {
        bearerToken = authHeader.split(" ");
        if(bearerToken.length == 2) {
            bucket.get(bearerToken[1], (error, result) => {
                if(error) {
                    return response.status(401).send({ "message": "Invalid session token" });
                }
                request.pid = result.value.pid;
                bucket.touch(bearerToken[1], 3600, (error, result) => {});
                next();
            });
        }
    } else {
        response.status(401).send({ "message": "An authorization header is required" });
    }
};

In the above example, we are checking the request for an authorization header. If we have a valid bearer token where the token is the session ID, we do a lookup. Remember, the session document has the profile ID in it. If the session lookup is successful, we’ll save the profile ID in the request, refresh the session expiration, and move through the middleware into the endpoint request.

If the session doesn’t exist, no profile ID will be passed and the request will fail.

If we wanted to use the middleware to get information about our profile, we could do the following:

app.get("/account", validate, (request, response) => {
    bucket.get(request.pid, (error, result) => {
        if(error) {
            return response.status(500).send(error);
        }
        response.send(result.value);
    });
});

Notice the validate happens first and then the rest of the request. The request.pidwas established by the middleware and it will get us a particular profile document for that ID.

Now maybe we want to create a blog article as this user. We might have an endpoint that looks like the following:

app.post("/blog", validate, (request, response) => {
    if(!request.body.title) {
        return response.status(401).send({ "message": "A `title` is required" });
    } else if(!request.body.content) {
        return response.status(401).send({ "message": "A `content` is required" });
    }
    var blog = {
        "type": "blog",
        "pid": request.pid,
        "title": request.body.title,
        "content": request.body.content,
        "timestamp": (new Date()).getTime()
    };
    bucket.insert(UUID.v4(), blog, (error, result) => {
        if(error) {
            return response.status(500).send(error);
        }
        response.send(blog);
    });
});

Assuming the middleware succeeded, we can create a blog object with a specific type and pid. Then, we can save it into the database.

Querying for all blog posts by a particular user isn’t too much different:

app.get("/blogs", validate, (request, response) => {
    console.log(request.pid);
    var query = N1qlQuery.fromString("SELECT `" + bucket._name + "`.* FROM `" + bucket._name + "` WHERE type = 'blog' AND pid = $pid");
    bucket.query(query, { "pid": request.pid }, (error, result) => {
        if(error) {
            return response.status(500).send(error);
        }
        response.send(result);
    });
});

Because we need to query by document property rather than document key, we’ll need to use an N1QL query, which uses the index we had created previously.

The document type and pid is passed into the query and all the documents for that particular profile are returned.

Not bad, right?

Conclusion

You just saw how to create a simple user profile store and session store using Node.js and NoSQL. This is a great follow-up to Kirk’s high-level explanation of the task in his previous article.

There is room for improvement in this example. As previously mentioned, the account documents could represent a form of login credentials where you could have a document for basic authentication, Facebook authentication, etc., all referring to the same profile document. Instead of using a UUID for the session, it could be a JSON Web Token (JWT) or something even more secure instead.

The next steps will be to create a client front-end for this example.

For more information on using Couchbase with Node.js, check out the Couchbase Developer Portal.

Learn how the world’s first NoSQL Engagement Database delivers unparalleled performance at any scale for customer experience innovation that never ends.

Topics:
database ,node.js ,nosql ,store ,tutorial ,couchbase

Published at DZone with permission of Nic Raboy, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}