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:
- Express JS
- Mongo DB Atlas as database
- Mongoose JS to handle MongoDB
- ejs
Let's make our hands dirty..
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)
Let's design our site next.. I made this simple design using Bootstrap
- As we are going to use ejs
npm install ejs
andnpm 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");
- 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"));
- 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);
});
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 }));
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) => { });
- 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");
});
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
- Get the url to be shortened from the user
- Assign the random ( or user requested ) short string
- Store short string and original url in databse with short string as Primarykey because shorturl must be unique
- 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
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
- We can check this
We are able to get user request now ๐app.post("/process", (request, response) => { console.log(request.body.fullUrl); console.log(request.body.shortUrl); }
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
- 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
- If you remember we decided to use shorturl as primarykey
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("/"); } }); } });
- getValidId function generates a random string that is not present yet in the database
```js
// 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;
}
```
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 });
});
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
Head over to heroku Node JS guide
- 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 )
- First register required domain name from any domain registrar ( I got mine from freenom . It offers free domain for 1 year so... )
- Then go to heroku dashboard and select your app
- Go to settings & scrolldown to Domains section
- Click Add New Domain and enter domain name
- Enter given DNS target to your domain or DNS Manager( I prefer to use cloudfare as CNAME record
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