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

Create a URL Shortener Using Ottoman ODM with Node.js and Couchbase

DZone's Guide to

Create a URL Shortener Using Ottoman ODM with Node.js and Couchbase

This time around we're going to explore the development process behind creating a URL shortener that uses Ottoman ODM, Couchbase Server, and Node.js.

· Web Dev Zone
Free Resource

Add user login and MFA to your next project in minutes. Create a free Okta developer account, drop in one of our SDKs to your application and get back to building.

Not too long ago I wrote about creating a URL shortener with Node.js and Couchbase N1QL. If you haven't already seen it, the article is a great introduction to Couchbase and creating RESTful APIs using Node.js and Express Framework. The catch behind that article, and it's not a bad catch, is that you have to prefer writing SQL queries to manage your NoSQL data.

What if you'd rather use an Object Document Model (ODM) rather than writing queries?

This time around we're going to explore the development process behind creating a URL shortener that uses Ottoman ODM, Couchbase Server, and Node.js.

The Requirements

There are not many requirements when it comes to the success of this application. For example, the following is what you'll need installed and configured:

  • Couchbase Server 4.1+
  • Node.js 4.0+

Since this is a Node.js application we'll need it installed and we'll be using the Node Package Manager (NPM) to gather all of our project dependencies. To get the most out of Ottoman we'll want to use Couchbase 4.1 or greater.

Understanding the Data Model and Preparing the Couchbase Database

For this example, we're going to create a bucket called example in Couchbase Server. You don't have to call it this, but throughout the article we'll be referencing it as such. No indexes or further setup is required on our cluster beyond the creation of the bucket.

In terms of data, the plan is to store one document for every URL that we wish to shrink. Each document might look something like this:

{ "_type": "Url", 
  "hash": "2xmx9qpyj", 
  "longUrl": "https://www.thepolyglotdeveloper.com/2016/05/using-couchbase-nosql-nativescript-angular-2-mobile-app/", 
  "shortUrl": "http://localhost:3000/2xmx9qpyj" 
}

The _type property is something defined by the Ottoman model, but the other three are properties that we're going to define. The hash is going to represent a unique hash of a value. This hash is going to act as the identifier in each of our short URLs.

Developing the Node.js URL Shortener Application

With the database ready to go and a basic understanding of our data model, we can work towards creating our Node.js URL shortener application. The application will be heavily dependent on Express Framework and a library called Hashids. We are going to use Hashids to save us the trouble of having to write our own short-hashing algorithm.

Creating the Project with the Dependencies

Let's start by creating a new Node.js project and installing all the required project dependencies. From the Terminal (Mac and Linux) or Command Prompt (Windows), execute the following:

npm init --y

The above command will create a new package.json file for our project. This file will hold all the dependency information as well as other things.

Now we need to install each of the required dependencies into our project:

npm install couchbase express ottoman body-parser hashids --save

The above command will install the Couchbase SDK, Express Framework, Ottoman, Hashids, and the Body Parser library for accepting body data in HTTP requests.

At this point we can start bootstrapping the application to get it ready for some heavy lifting.

Bootstrapping the Application

To keep things simple, all of our application code will reside in a single file within our project called index.js. In a production environment you'd probably want to break it up for maintainability, but for this very simple example we'll be fine.

Open the index.js file and include imports to each of the dependencies that we installed:

var Couchbase = require("couchbase"); 
var Ottoman = require("ottoman"); 
var Express = require("express"); 
var BodyParser = require("body-parser"); 
var Hashids = require("hashids");

With the dependencies imported, we can now initialize them for use within the application:

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

In the above example, we're essentially saying that we want to initialize Express Framework and accept JSON and URL encoded body data from requests.

At this point we can establish a connection to our Couchbase cluster and open the application bucket:

Ottoman.bucket = (new Couchbase.Cluster("couchbase://localhost")).openBucket("example");

To use Ottoman certain indexes need to exist. Lucky for us we can have Ottoman create these indexes for us if they don't already exist. After we're sure they exist, we can start serving the application.

Ottoman.ensureIndices(function(error) 
                      { 
  if(error) { console.log(error); } 
  var server = app.listen(3000, function() 
                          { 
    console.log("Listening on port %s...", 
                server.address().port); }); });

In our example, the application will serve locally on port 3000.

Up until now we only have the base framework up and running for our URL shortener application. We don't actually have any shrinking logic or interaction with the database in regards to CRUD or query operations.

Creating the Data Model

We know how the data will look in the database, but we don't yet know how to create it. Since we're using an ODM rather than N1QL queries we need to define the model within our code.

In this particular project, the model is very simplistic. It would look like this:

var UrlModel = Ottoman.model("Url", { hash: {type: "string", readonly: true}, longUrl: "string", shortUrl: "string" }, { id: "hash" });

In the above data model, which we're calling Url, we have three properties. The hash property will represent our document id. It is not a requirement to manually set the document id, but for this project it makes sense. Manually setting a document id requires the property to be read-only. The other two properties are standard string values.

The model can be infinitely more complex depending on your needs.

Applying the Data Model in a Series of RESTful Endpoint Methods

The application we're building is a RESTful web service so we need to create a few endpoint functions. The application will contain the following three public endpoints:

/ /create /expand

When the root endpoint is hit, the application will navigate us to the long URL that is stored in Couchbase. When the /create endpoint is hit we will attempt to find a previously saved short URL or create one if it doesn't exist. We do this because we don't want to save multiple copies of a long URL. Finally, we have an /expand endpoint so we can see where our short URL goes.

Starting with the /create endpoint, we have the following method, which is probably the most complex part of our application:

app.post("/create", function(req, res) { if(!req.body.longUrl) { return res.status(400).send({"status": "error", "message": "A long URL is required"}); } UrlModel.find({"longUrl": req.body.longUrl}, function(error, url) { if(error) { return res.status(400).send(error); } if(url.length == 0) { var hashids = new Hashids(); var urlObj = new UrlModel({ hash: hashids.encode((new Date).getTime()), longUrl: req.body.longUrl }); urlObj.shortUrl = "http://localhost:3000/" + urlObj.hash; urlObj.save(function(error, result) { if(error) { return res.status(400).send(error); } res.send(urlObj); }); } else { res.send(url[0]); } }); });

The above endpoint is a POST request endpoint that expects a body. If no long URL is found in the body, an error will be returned. Otherwise, an Ottoman lookup will happen based on the provided long URL. If data is found in the lookup, that data will be returned to the user, otherwise a process will start for creating a new entry.

During the new entry creation, a unique hash is created based on the timestamp. This has is appended to a short hostname and saved to the database along with the hash itself and the long URL. The object is returned after a successful save.

When it comes to the /expand endpoint we have the following:

app.get("/expand", function(req, res) 
        { if(!req.query.shortUrl) 
        { return res.status(400).send(
          {"status": "error", "message": "A short URL is required"}); } 
         UrlModel.find({"shortUrl": req.query.shortUrl}, 
                       function(error, url) 
                       { if(error) 
                       { return res.status(400).send(error); } 
                        res.send(url.length > 0 ? url[0] : {}); }); });

The method is similar to what we saw in the /create endpoint, but not quite the same. Instead we do a lookup based on the provided short URL. If found, return the document which contains the long URL, otherwise return an empty object.

Finally we have the root endpoint. This endpoint is responsible for redirection to the long URL and requires an id which in reality is our hash. Remember, we're hosting the API and short URLs on the same server.

app.get("/:id", function(req, res) 
        { 
  if(!req.params.id) 
  { 
    return res.status(400).send(
      {"status": "error", "message": "An id is required"}); 
  } 

  UrlModel.getById(req.params.id, function(error, url) 
                   { if(error) 
                   { return res.status(400).send(error); 
                   } 
                    res.redirect(url.longUrl); }); });

So let's say we navigate to http://localhost:3000/2xmx9qpyj in the browser. The 2xmx9qpyj is our id which is also our hash value. We can do a document lookup based on this id and navigate if it exists.

Not too bad right?

The Full Application Source Code

In case you'd like to see the entire application brought together, you can find it below.

var Couchbase = require("couchbase"); 
var Ottoman = require("ottoman"); 
var Express = require("express"); 
var BodyParser = require("body-parser"); 
var Hashids = require("hashids"); 
var app = Express(); 
app.use(BodyParser.json()); 
app.use(BodyParser.urlencoded({ extended: true })); 
Ottoman.bucket = (
  new Couchbase.Cluster("couchbase://localhost")).openBucket("example"); 
var UrlModel = Ottoman.model("Url", { hash: {type: "string", readonly: true}, 
  longUrl: "string", shortUrl: "string" }, { id: "hash" }); 
app.get("/expand", function(req, res) 
        { if(!req.query.shortUrl) 
        { return res.status(400).send({"status": "error", 
          "message": "A short URL is required"}); 
        } 

         UrlModel.find({"shortUrl": 
                        req.query.shortUrl}, 
                       function(error, url) 
                       { 
                         if(error) 
                         { 
                           return res.status(400).send(error); 
                         } 
                         res.send(url.length > 0 ? url[0] : {}); }); }); 
app.post("/create", function(req, res) 
         { 
           if(!req.body.longUrl) 
           { 
             return res.status(400).send(
               {"status": "error", "message": "A long URL is required"}); 
           } 
           UrlModel.find({"longUrl": req.body.longUrl}, 
                         function(error, url) 
                         { 
                           if(error) 
                           { return res.status(400).send(error); } 
                           if(url.length == 0) 
                           { var hashids = new Hashids(); 
                            var urlObj = new UrlModel(
                              { hash: 
                               hashids.encode((new Date).getTime()), 
                                 longUrl: req.body.longUrl }); 
                            urlObj.shortUrl = "http://localhost:3000/" +
                              urlObj.hash; urlObj.save(function(error, result) 
                                                       { 
           if(error) {
             return res.status(400).send(error); } 
           res.send(urlObj); }); } 
                           else 
                           { 
                             res.send(url[0]); } }); }); 
app.get("/:id", function(req, res) 
        { 
          if(!req.params.id) 
{ return res.status(400).send(
  {"status": "error", "message": "An id is required"}); } 
          UrlModel.getById(req.params.id, function(error, url) 
                           { if(error) { return res.status(400).send(error); }
                            res.redirect(url.longUrl); }); }); 
Ottoman.ensureIndices(function(error) 
                      { if(error) 
                      { console.log(error); }

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

If you run the above code with npm index.js you can then start using each of the API endpoints found at http://localhost:3000.

Conclusion

You just saw how to create a URL shortener application using an Object Document Model (ODM) called Ottoman, Couchbase Server, and Node.js. If you're not a fan of ODMs or you want to check out how to do this with SQL queries, you can check out a previous tutorial I wrote on the subject.

Want to take this tutorial a step further? Why not try to collect analytics information from people trying to use the short URLs. You could keep a hit counter inside the documents before navigation or even store information like browser user agents.

Launch your application faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
nodejs ,nosql ,couchbase ,web dev

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

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}