DEV Community

Cover image for Building a URL shortening service with NodeJS and MongoDB. Deploy to Azure.
Olamide Aboyeji
Olamide Aboyeji

Posted on

Building a URL shortening service with NodeJS and MongoDB. Deploy to Azure.

Hey there, we would be building a URL shortening service with NodeJS, Express, and MongoDB. We would then go ahead and deploy our web application to Azure. It would be code a along tutorial and I would be explaining each line of code.

Link to the demo and GitHub repository would be added at the end.

Excited right ?
Excited

What should I know/have

  • Basic understanding of HTML, CSS, and Javascript
  • Have NodeJS installed on your computer (install here)
  • Have MongoDB installed on your computer (install here)
  • Experience creating GitHub repositories, and pushing your local repository to the remote one.

Let's get started

First, let's create a folder for our application. We'd call it url-shortener.
Mkdir && cd

Then in the terminal run npm init.
npm run init
This would create a package.json file for us.

Now let's install the packages we would be using.
npm i
npm I dev
express: Node.js framework that provides a robust set of features for web and mobile applications.

body-parser: To parse incoming request bodies before your handlers.

mongoose: Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment.

nodemon: This is used to automatically restart our server, so we would not have to stop and restart the server each time we make a change. We are installing this as a dev dependency because we only need it in development.

When the installs are done, edit the main and scripts of your package.json to look like below.

{
  "name" : "url-shortener",
  "version" : "1.0.0",
  "description" : "URL shotener web app",
  "main" : "server.js",
  "scripts" : {
    "dev" : "nodemon server.js",
    "start" : "node server.js"
  },
  "keywords" : ["URL", "shortener"],
  "author" : "Your name",
  "dependencies" : {
    "express" : "^4.17.1",
    "mongoose" : "^5.9.7",
    "body-parser" : "^1.19.0"
  },
  "devDependencies" : {
    "nodemon" : "^2.0.2"
  }
}

Front end

We would be using a very basic UI.
For the front end of the app, create a folder called public in our working directory. This is where we would have our front end files(HTML, CSS, and Javascript). Create files named index.html, style.css, and main.js in the public folder. The content of our index.html and style.css are shown below :

index.html :

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" 
              content="width=device-width, 
              initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <link rel="stylesheet" href="style.css">
        <title>URL shortener</title>
    </head>
    <body>
        <form id="url-form">
            <h1 class="header">URL SHORTENER</h1>
            <p class="desc">Shorten your long URL to 
             <span class="domain">mydomain.com</span>/unique_name
            </p>
            <p>
                <input required class="url-input" 
                id="original-url" type="url" 
                placeholder="paste original URL here">
            </p>
            <input disabled class="base-url" value="">
            <input required class="unique-input" id="unique-name" 
             type="text" placeholder="unique name">
            <p id='status'><button>SHORTEN</button></p>
            <p id="confirmationShow"></p>
        </form>
    </body>
    <script>
      const domain = window.location.host;
      document.querySelector('.domain').innerText = domain;
      document.querySelector('.base-url').value = domain;
    </script>
    <script src="main.js"></script>
</html>

style.css :

body{
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    background : linear-gradient(to right, #aa5f15, #542008);
}
html, body {
    font-family: Verdana, Geneva, Tahoma, sans-serif;
    overflow: hidden;
    height: 100%;
}
form{
    border: red;
    padding-top: 15vh
}
.a {
    color : white;
}
.header{
    color: bisque;
    letter-spacing: 3px;
    font-size: 3rem;
    margin-bottom: 1px;
}
.header span {
    font-style: italic;
}
.desc{
    margin-top :2px;
    color: bisque;
}
.base-url{
    padding: 10px;
    background-color: #a7a7a7;
    border-radius: 8px 0 0 8px;
    border: 1px solid black;
    width: 100px;
    font-weight: bold
}

.unique-input{
    padding: 10px;
    border-radius: 0 8px 8px 0;
    outline: none;
    border: 1px solid black;
}
.url-input{
    border-radius: 8px;
    padding: 10px;
    width: 300px;
    outline : none;
}

button{
    background-color: burlywood;
    padding: 10px;
    border-radius: 10px;
    outline: none;
    cursor: pointer;
}

#confirmationShow {
    font-style: italics;
}

.loader {
    border: 8px solid #f3f3f3;
    border-radius: 50%;
    border-top: 8px solid orange;
    width: 10px;
    height: 10px;
    -webkit-animation: spin 2s linear infinite;
    animation: spin 2s linear infinite;
    margin: 8px auto !important;
}

@-webkit-keyframes spin {
    0% { -webkit-transform: rotate(0deg); }
    100% { -webkit-transform: rotate(360deg); }
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

Server

Create a file server.js in the root directory. And add the following

server.js :

//Import modules
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');

//Call the express function to initiate an express app
const app = express();

//This tells express to parse incoming requests
app.use(bodyParser.json());

//This tells express we are serving static files (front end files)
app.use(express.static(path.join(__dirname, 'public')));

/** NB: process.env.PORT is required as you would 
not be able to set the port manually in production */
const PORT = process.env.PORT || 3000;

//app to listen to specified port
app.listen(PORT, () => {
  console.log(`Server running on port${PORT}`);
});
Note that path is an inbuilt node module and does not need to be installed

In the terminal, run npm run dev.
You should see this
terminalResult

Open your browser and go to http://localhost:3000. This should show up.
FrontEnd

Yayy, our public page is being served.

Now to the next part

Let's connect to our MongoDB

Create a file called db.js in the root directory, and put this in it.

db.js :

//import mongoose library
const mongoose = require('mongoose');

//MONGO_URI 
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/url'; 

//Connect to DB function
const connect = () => {
  mongoose.connect(MONGO_URI, {useNewUrlParser : true, useUnifiedTopology : true})
  .then(() => console.log('DB connected'))
  .catch(err => console.log(err));
  //On connection error, log the message
  mongoose.connection.on('error', err => {
    console.log(`DB connection error : ${err.message}`);
  });
}

//export the connect function, to use in server.js
module.exports = { connect }; 

Now let's go back to our server.js, and implement the connection to database feature

server.js :

//Import modules
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');

//Import db module
const db = require('./db.js');

//Call the express function to initiate an express app
const app = express();

//Connect to database by calling our connect method
db.connect();

//This tells express to parse incoming requests
app.use(bodyParser.json());

//This tells express we are serving static files (front end files)
app.use(express.static(path.join(__dirname, 'public')));

/** NB: process.env.PORT is required as you would 
not be able to set the port manually in production */
const PORT = process.env.PORT || 3000;

//app to listen to specified port
app.listen(PORT, () => {
  console.log(`Server running on port${PORT}`);
});

Make sure your local Mongo server is running.
On server restart, you should see this in terminal
terminal

Create URL Model

Now that we have successfully connected to our database, let's create the URL model that would hold the format of how we want to store URLs in the database.

Create a file called url.model.js, and put this.

url.model.js :

const mongoose = require('mongoose');

//create Url Schema (format)
const urlSchema = new mongoose.Schema({
    originalUrl: {
        type : String,
        required : true
    },
    shortUrl : {
        type : String,
        required : true
    },
    unique_name : {
        type : String,
        required : true
    },
    dateCreated : {
        type : Date,
        default : Date.now
    }  
});
//Use schema to create a Url model
const Url = mongoose.model('Url', urlSchema);

//Export Url Model
module.exports = Url;

Create Controllers to handle all routes

We would now create controllers that would handle our two routes :

  • createShortLink
  • openShortLink Create a file called url.controllers.js and add code below :

url.controller.js :

//import Url model
const Url = require('./url.model.js');

//This is basically your domain name
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';

const createShortLink = async (req, res) => {
    //get the originalUrl and unique_name from the request's body
    let { originalUrl, unique_name } = req.body;

    try {
        //check if unique_name alredy exists
        let nameExists = await Url.findOne({ unique_name });
        /** if unique_name already exists, send a response with an
        error message, else save the new unique_name and originalUrl */
        if(nameExists){
            return res.status(403).json({
                error: "Unique name already exists, choose another",
                ok : false
            }) 
        }
        else {
            const shortUrl = baseUrl + '/' + unique_name;
            url = new Url({
                originalUrl,
                shortUrl,
                unique_name
            });
            //save
            const saved = await url.save();
            //return success message shortUrl
            return res.json({
                message : 'success',
                ok : true,
                shortUrl
            });
        }
    } catch (error) {
        ///catch any error, and return server error
        return res.status(500).json({ok : false, error : 'Server error'});
    }
};

const openShortLink = async (req, res) => {
    //get the unique name from the req params (e.g olamide from shorten.me/olamide)
    const { unique_name } = req.params;

    try{
      //find the Url model that has that unique_name
      let url = await Url.findOne({ unique_name });

       /** if such Url exists, redirect the user to the originalUrl 
       of that Url Model, else send a 404 Not Found Response */
        if(url){
            return res.redirect(url.originalUrl);
        } else {
            return res.status(404).json({error : 'Not found'});
        }  
    } catch(err) {
       //catch any error, and return server error to user
        console.log(err);
        res.status(500).json({error : 'Server error'});
    } 
};

module.exports = {
    createShortLink, openShortLink
}

Configure routes

Let's go back to server.js and use these controllers we just created in our routes.
We would first import them and use as shown below.

server.js :

//Import modules
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');

//Import db module
const db = require('./db.js');

//Import controllers
const { createShortLink, openShortLink } = require('./url.controller.js');

//Call the express function to initiate an express app
const app = express();

//Connect to database by calling our connect method
db.connect();

//This tells express to parse incoming requests
app.use(bodyParser.json());

//This tells express we are serving static files (front end files)
app.use(express.static(path.join(__dirname, 'public')));

//USE CONTROLLERS
//route to create short link
app.post('/createShortLink', createShortLink);
//route to open short link, ':' means unique_name is a param
app.get('/:unique_name', openShortLink);

/** NB: process.env.PORT is required as you would 
not be able to set the port manually in production */
const PORT = process.env.PORT || 3000;

//app to listen to specified port
app.listen(PORT, () => {
  console.log(`Server running on port${PORT}`);
});

Yayy we have come a long way!!

Now let us start making requests from our frontend.

Open the public/main.js file and add this :

main.js :

const urlForm = document.getElementById('url-form');
const originalUrl = document.getElementById('original-url');
const uniqueName = document.getElementById('unique-name');
const confirmationShow = document.getElementById('confirmationShow');
const status = document.getElementById('status');

const formSubmit = e => {
    e.preventDefault();
    status.innerHTML = '<button type="button" class="loader"></button>'
    fetch('/createShortLink', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          originalUrl : originalUrl.value,
          unique_name : uniqueName.value
        })
    })
    .then(data => data.json())
    .then(response => {
        status.innerHTML = '<button>SHORTEN</button>'
        if(!response.ok){
            confirmationShow.innerText = response.error;
        }
        else {
            confirmationShow.innerHTML = `Hooray!!! The link can now be visited 
            through <a target="_blank" 
            href=${response.shortUrl} rel = "noopener noreferer" > 
            ${response.shortUrl} </a>`;
        }
    })
    .catch(err => {
        console.log('oops', err);
        status.innerHTML = '<button>SHORTEN</button>';
        confirmationShow.innerText = 'Network error, retry'
    })
};

urlForm.addEventListener('submit', formSubmit);

THAT'S IT !!!

Now make sure your server is running, open up your browser, go to http://localhost:3000. Type a long URL in the original URL field, and a unique name in the unique name field. Submit your form and watch the magic happen.

donee

NEXT STEP

GitHub Repository

Create a GitHub repository for the project and push your project to the remote repository (Follow this guide)

MongoDB Server

Before we deploy our project to Azure, we need to have a remote MongoDB server, because Azure cannot connect to the database on our local server. Head over to MongoDB Atlas and get your connection string. This would be our MONGO_URI variable on the server. (Remember when we added process.env.MONGO_URI) to our app. You can follow this guide to get your connection string.

LAST STEP!!!

Deloy to Azure

  • Head over to Azure Portal and create an account. NOTE : When you register on Azure, you would get $200 credits to try Azure for 30 days. A credit card validation would be required. If you are a student, Click here to create an account for free with no credit card required.

thank you

And that's it. Our app is liveeeee!!!

Go to your site's URL and test it out.

Follow this guide to buy and set up an actual short custom domain for the app. I got rdre.me for mine.

You can go on and add more features to your app, such as registering users before they can create a short link, short link expiry date, etc.

Thank you for coming this far.

thank you

Link to Demo : https://rdre.me

Link to GitHub repository : https://github.com/aolamide/shorten-url

Please drop your comments and questions.

You can reach me on LinkedIn and Twitter.

Latest comments (17)

Collapse
 
louisnel profile image
Louis Nel

Loved this tutorial. The hardest part seems to find a cool short URL. The use of the fetch library in the frontend seems leaner than using jQuery AJAX.

Collapse
 
petermezes profile image
Peter Mézeš

Nice one, thank you!

Btw, that error handling:

catch (error) {
        ///catch any error, and return server error
        return res.status(500).json({ok : false, error : 'Server error'});
}

It is like programming version of: - What is wrong darling? - Nothing!

All good, just made me giggle a bit :)

Collapse
 
emindeniz99 profile image
Emin Deniz

👏🏼 Good project

I can’t understand why you are storing “shorturl” and “ uniquename”, shorturl consist of baseurl and uniqueurl
It can be generated dynamically when execution of geturl, because baseurl is constant for all links, no need to store it.
Also, if we change the domain, it can be used still

Collapse
 
andersonpimentel profile image
Anderson Pimentel

Great article! Congrats!

Two minor fixes:

  • On package.json file, it's missing a comma at the end of the line:
"dev" : "nodemon server.js"
  • On server.js there's an extra s on body-pa*s*rser
Collapse
 
olamideaboyeji profile image
Olamide Aboyeji

Thanks for pointing that out. Fixed.

Thank you

Collapse
 
jopo79 profile image
Javier Fernández

Good Job!!

Collapse
 
olamideaboyeji profile image
Olamide Aboyeji

Thank you

Collapse
 
sammychinedu2ky profile image
sammychinedu2ky

Nice one bro

Collapse
 
olamideaboyeji profile image
Olamide Aboyeji

Thanks

Collapse
 
blgguy profile image
Aminu M. Bulangu

Awesome

Collapse
 
olamideaboyeji profile image
Olamide Aboyeji

Thank you

Collapse
 
patarapolw profile image
Pacharapol Withayasakpunt

The problem happens to be having to buy a short domain, to get a short URL.

Collapse
 
emindeniz99 profile image
Emin Deniz

Sometimes it is not necessary. At my student club, we are using rebrandly with subdomain of our website. example go.compec.org/xyz
It seems long, but easier than bit.ly or tinyurl. Domain part of short url includes “go” and our club short name.
We are happy with it.

Collapse
 
patarapolw profile image
Pacharapol Withayasakpunt

Actually, xxx.now.sh seems shorter (with splat / regex), but I don't how short xxx can be.

Also, now.sh supports database via /api folder (or serverless / buildpacks). I should try to make a url shortener someday, perhaps...

Collapse
 
olamideaboyeji profile image
Olamide Aboyeji

Yes, you would have to set that up on the azure portal. Buy one and configure.

Collapse
 
vicradon profile image
Osinachi Chukwujama

The short url is long

😄

Nice article. Though we'd need to make your Frontend more appealing.

Collapse
 
olamideaboyeji profile image
Olamide Aboyeji

Thank you!
Yes, you would have to get a short domain name and map it to the azure app.

For the frontend 😅, did not want to spend time on UI, just straight to implementation.