DEV Community

Cover image for Build and Deploy URL Shortener to custom domain from scratch -  Node JS
Rajasekhar Guptha
Rajasekhar Guptha

Posted on

Build and Deploy URL Shortener to custom domain from scratch - Node JS

Let's build a URL shortener ( Minii in my case ) using Node JS (Express JS)

Structure:

minii_stack.PNG

  • Express JS
  • Mongo DB Atlas as database
  • Mongoose JS to handle MongoDB
  • ejs

Let's make our hands dirty..

  1. Create Folder with project name ( Minii in my case ) npm init in terminal and enter your details like below
  • You can leave everything default
  • I chose server.js as entry point by default it is index.js
package name: (minii)
version: (1.0.0)
description: Custom URL shortener
entry point: (index.js) server.js
test command:
git repository:
keywords:
author: Rajasekhar Guptha
license: (ISC)
About to write to E:\WebD\minii\package.json:

{
  "name": "minii",
  "version": "1.0.0",
  "description": "Custom URL shortener",
  "main": "script.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Rajasekhar Guptha",
  "license": "ISC"
}


Is this OK? (yes)

Enter fullscreen mode Exit fullscreen mode
  1. Let's design our site next.. I made this simple design using Bootstrap minii_proto.PNG
  • As we are going to use ejs
  • npm install ejs and npm install express in terminal ( documentation here )
  • create public and views folders.
  • then create index.ejs file in views folder to design our page
  • and then add css files inside public folder
  • all these html and css files will be there at the end of this post
  • create server.js in the root directory
    • now we have to setup express and ejs as view engine
const express = require("express");

// app setup
const app = express();

app.set("view engine", "ejs");

Enter fullscreen mode Exit fullscreen mode
  • now define static folder to express
    • all our css files and assets were static files and we have to tell express about them to treat them as static
const express = require("express");

// app setup
const app = express();

app.set("view engine", "ejs");
// views folder
app.set("views", __dirname + "/views");

// setup static folder
app.use(express.static(__dirname + "/public"));

Enter fullscreen mode Exit fullscreen mode
  • Now we need to assign the port for our app to run
  • During development stage we can hardcode 3000 or 5000 But in production stage we cannot decide & hardcode because it will be allotted dynamically But we can get the assigned port using

process.env.PORT

  • it is null if the app is not in production stage, so the logic is

var port = process.env.PORT;
if (!port) {
  port = 3000;
}
app.listen(port, function () {
  console.log("Server is up on port : " + port);
});

Enter fullscreen mode Exit fullscreen mode
  • The basic setup is over. Now we will start catching requests for our page
    Firstly catch get request to our home page

  • For this we need body-parser, setup goes like this

const bodyParser = require("body-parser");
....

// to get url details we need this
app.use(bodyParser.urlencoded({ extended: true }));

Enter fullscreen mode Exit fullscreen mode

Now we are ready to catch url requests

  • app.get("path",callback fun) for get req
  • app.post("path",callback fun) for post req General representation for callback function is
       (request, response) => {

      });

Enter fullscreen mode Exit fullscreen mode
  • Request arg contains details of request
  • We will send our result using response arg

in our case when we received a get req for our home page we want that index.ejs to be rendered and displayed.. So,

app.get("/", (request, response) => {
  response.render("index");
});
Enter fullscreen mode Exit fullscreen mode

Now we can test our home page using

run node server.js and head to *localhost:3000 *

Yeah..! πŸŽ‰We completed our first major step ✨

There is one problem with "node server.js" i.e whenever we made some changes to script to make them apply we need to restart the server every time.
So I suggest using "nodemon" - automatically restarts the app whenever file changes in the directory are detected.
Install it as development dependcency as we don't need this in production environment.
' npm install --save-dev nodemon '

From now onwards use " nodemon server.js " instead of "node server.js" to start app.

We finished our setup and let us look at core functionality

Working behind the scene

  1. Get the url to be shortened from the user
  2. Assign the random ( or user requested ) short string
  3. Store short string and original url in databse with short string as Primarykey because shorturl must be unique
  4. Whenever we received a request to path similar to /shortstring check the database for respective original url and redirect to that. If doesn't exist return 404 error
  1. Getting the URL to be shortened
    add form to home page with method post and action to /process. ( action path is your wish )

          <form
          action="/process"
          method="post"
        > <input name="fullUrl"></input><input name="shortUrl"></input>
       </form>
    
  • Whenever user submit form we can catch & process the request in server.js file like this

       app.post("/process", (request, response) => {
    
      }
    
    • user filled values can be obtained from request arg like
      request.body.name  - name : given for input fields

     // In our case 
      request.body.fullUrl    
      request.body.shortUrl

Enter fullscreen mode Exit fullscreen mode
  • We can check this
         app.post("/process", (request, response) => {
           console.log(request.body.fullUrl);
           console.log(request.body.shortUrl);
        }
Enter fullscreen mode Exit fullscreen mode

We are able to get user request now πŸŽ‰

  1. Let us add Database to our app now

    • I prefer to use mongodb database in Mongo Atlas ( check setup here )
    • Install mongoose

      • npm install mongoose
      • setup mongoose in app
      
         const mongoose = require("mongoose");
         // mongo atlas setup
       mongoose.connect(  
       "mongoose_link",
      {
        useNewUrlParser: true,
       useUnifiedTopology: true,
      }
      );
      
  • replace above mongoose_link with your own.
    To get your link

    • Go to your Cluster dashboard in Mongo Atlas cluster.PNG
    • Click Connect > Connect Your Application and then copy your link and replace Password and dbname with your password and database name

    Succesfully connected database to application.

    • Now, we have to design our database model schema

      • If you remember we decided to use shorturl as primarykey
      const urlDbSchema = mongoose.Schema({
      _shortUrl: {
      type: String,
      require: true,
      },
      fullUrl: {
      type: String,
      require: true,
      },
      count: { type: Number, default: 0 },
      });
      
      • connect this model to DB so that we can use

      const urlsDb = mongoose.model("urls", urlDbSchema);

  • Now, our database is ready to operate.So, let us complete our post request processing with database
    app.post("/process", async (request, response) => {
      const userReqString = request.body.shortUrl;
      if (userReqString) {
      // user requested some string

      // checking if requested string is not taken already
    /f (await urlsDb.where({ _shortUrl: userReqString }).countDocuments > 0) {
       // if already exists redirecting to home page
       response.redirect("/");
        } 
      else {
      // requested string available

      // create new entry to insert to DB
      const temp = new urlsDb({
        fullUrl: request.body.fullUrl,
        _shortUrl: request.body.shortUrl,
      });
      urlsDb.insertMany(temp, (error) => {
        if (error) {
          //{ error: "Oops..! Backend Error" },
          response.redirect("/");
        } else {
          // success
          response.redirect("/");
            }
          });
        }
      } else {
       // user not requested any string 
       // assign a random string
        const temp = new urlsDb({ fullUrl: request.body.fullUrl, _shortUrl: 
    getValidId() });
      urlsDb.insertMany(temp, (error) => {
        if (error) {
          //{ error: "Oops..! Backend Error" },
            } else {
          // success
          response.redirect("/");
            }
          });
        }
          });

Enter fullscreen mode Exit fullscreen mode
  • getValidId function generates a random string that is not present yet in the database

        // getValidId()
       function getValidId() {
      var randomId = getRandomId();
      while (urlsDb.where({ _shortUrl: randomId }).countDocuments > 0) {
        // console.error("still in while");
        randomId = getRandomId;
      }
      // console.log("random " + randomId);
      return randomId;
    }

    function getRandomId() {
      allowedChars =
      "_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
      var randomStr = "";
      for (var i = 0; i < 13; i++) {
        randomStr += allowedChars[Math.floor(Math.random() * 
        allowedChars.length)];
        }

         return randomStr;
        }


Enter fullscreen mode Exit fullscreen mode

We almost completed our app
The one thing left behind is to handle shorturl and redirect it to original one.

  • When user requested some short url we will get a get request for that particular url
  • But scripting function to handle every url's get request is impossible.So we have an option to generalize this

app.get("/:keyword",callback) - handles get req for all urls in the form website.com/abcd.. and

this path string ( abcd here ) can be obtained from request.params.keyword

 app.get("/:shorturl", async (request, response) => {
  const shorturl = request.params.shorturl;
  await urlsDb
    .findOne((error, result) => {
      if (error) {
       // database error
        response.send(error);
      } else {
        if (result) {
        // redirect to original url (Http Status code-301)
        response.redirect(result.fullUrl);
        }
      }
    })
    .where({ _shortUrl: shorturl });
});

Enter fullscreen mode Exit fullscreen mode

That's it.. Congro πŸŽ‰ we build our application πŸ₯³

The major step is Deploying.I want to deplot this for free as this is not for commercial purpose

  • I decided to deploy to heroku also I didnot find any better free alternatives to deploy Node JS applications
  1. Head over to heroku Node JS guide
  2. Follow the steps until you deploy the app Your app is on Internet now πŸ₯³πŸ₯³ But some people ( like me ) want to have this on custom domain (like mine minii.ml/ )
  3. First register required domain name from any domain registrar ( I got mine from freenom . It offers free domain for 1 year so... )
  4. Then go to heroku dashboard and select your app
  5. Go to settings & scrolldown to Domains section
  6. Click Add New Domain and enter domain name
  7. Enter given DNS target to your domain or DNS Manager( I prefer to use cloudfare as CNAME record

cname.PNG

If your choose to add this to subdomain like subdomain.domain.com
place subdomain as domain name for root domains like domain.com place @ in domain name . and place DNS target given in heroku here in target.

After some time your app will be active on your domain..
You succesfully created your own url shortener for free on domain of your wish πŸ₯³πŸ₯³πŸŽ‰πŸŽ‰
If you like this Share the post
Like this post and comment to get next post on How to add some additional features to this app like displaying error , user's shortened links in a table etc

Top comments (0)