How to build a URL shortener?
talent
Posted on March 14, 2023
Does the market need another URL shortener? Probably not! But, why build it? A simple reason is to understand the complexities, along with the programming concepts, and deal with the problems that we face along.
The entire flow is shown below:
Start with the nodejs backend → Write APIs that can create the data in DB → write react frontend → call the API with URL → store the URL → shorten the URL → write original_url_id with the shortened URL and return the shortened URL
As this is completely a backend-heavy project, proceed ahead if you are familiar with nodejs concepts. Of course, a basic understanding is enough to build this application. And I will be using PostgreSQL as the database.
Start with npm init inside a folder where you are starting your project. Install the express, cors, and pg (for PostgreSQL) with the npm command.
Create a file index.js and start writing the APIs in this file. I won’t be using any ORMs and will be using SQL queries for performing CRUD.
const express = require('express');
const cors = require('cors'); // to avoid cors error
const app = express();
const PORT = 3001;
app.use(cors());
app.use(express.json());
// we will write all services in a services folder
const urlService = require('./services/urlService');
// connect to your db
const {client} = require('./db-connection');
/*
-----------------------------------------
Write it in a file and export it
-----------------------------------------
const {Client} = require('pg');
const client = new Client({
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'DATABASE PASSWORD',
database:'DATABASE NAME'
});
client.connect((err) => {
if (err) {
console.log('Error in connection', err);
}
console.log('Connection Successful!');
});
module.exports={client};
-----------------------------------------
*/
// API for shortening the url
app.get('/data', async (req, res) => {
const url = req.query.url;
// insert the URL into db
const insert = await client.query(`insert into url (url) values($1) RETURNING *`,[URL]);
// get the inserted row id -is a foreign key after shortening the url
const insertedRowId = insert.rows[0].url_id;
// short the URL with the help of a service
const shortUrl = await urlService.shortenUrl(insertedRowId);
if (shortUrl) {
// at present you can use your local server if deployed can use domain name
const result = 'http://localhost:3001/p/' + shortUrl;
res.json({success: true, message: result});
} else {
res.json({success: false, message: 'Error occured while shortening the url in /data'});
}
});
//There is still more
app.listen(PORT, () => {
console.log('Server is up and running at the port:', PORT);
});
It not enough, just by shortening the URL. Once the URL is shortened it must be given to the user to copy and then when they paste that URL in the browser, or when shared as link, the link must successfully redirect. So before proceeding ahead lets write the service for shortening the URL.
Create a folder called services. And create a file inside this folder called urlService.js
Once the project grows it's important for you to maintain a proper folder structure for maintenance and scalability.
// we will be using the crypto module
const crypto = require('crypto');
// you need db connection as you will perform read & write operation form here
const {client} = require('../db-connection');
const self = module.exports = {
shortenUrl: async (id) => {
try {
const Data = await client.query(`select * from url where url_id=${id}`);
if (Data && Data.rows[0].url) {
// .createHash() returns the hash based on the specified algorithm
const hash = crypto.createHash('sha1');
// update() is called to add the data to the hash
hash.update(Data.rows[0].url);
// generate the final hash value
const hexDigest = hash.digest('hex');
// get certain portion of a string
const shortHexDigest = hexDigest.substr(0, 6);
const randomInt = Math.floor(Math.random() * Math.pow(62, 6));
// write a function convertbase to get the base62 value
const base62 = await self.convertBase(randomInt, 62);
// concat base62 with the shortHexDigest
const shortUrl = base62.padStart(6, '0') + shortHexDigest;
// insert the short URL into DB
const insertShortUrl = await client.query(`insert into short_urls (url_id, short_url) values($1, $2)`, [id, shortUrl])
return shortUrl;
}
return false
} catch (err) {
console.log('Error in the function shortenUrl', err);
}
},
convertBase: async(num, base) => {
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let converted = '';
while (num > 0) {
const digit = num % base;
converted = alphabet[digit] + converted;
num = Math.floor(num / base);
}
return converted;
},
// more to go
}
The createHash() method is used to create a hash object which can be used to generate a cryptographic hash of a given message or data.
The update() method can be called multiple times to add more data to the hash computation. It takes a parameter that represents the data to be added to the hash. This parameter can be of type string, Buffer, or TypedArray.
The digest() method is called to generate the final hash value.
const randomInt = Math.floor(Math.random() * Math.pow(62, 6));
The Math.random() function returns a random floating-point number between 0 (inclusive) and 1 (exclusive). Multiplying this value by Math.pow(62, 6) generates a random floating-point number between 0 (inclusive) and 62^6 (exclusive).
The Math.floor() function rounds down this floating-point number to the nearest integer. This ensures that the result is an integer between 0 (inclusive) and 62^6 (exclusive).
The above code generates a random integer that can be used as an identifier or token with 56,800,235,584 possible values, which is a large enough range to make it highly unlikely that two random integers generated by this code will be the same.
padStart() is a string method in JavaScript that adds characters to the beginning of a string until it reaches a specified length. It can be used to pad a string with leading zeroes or other characters.
Now let's write the API for the URL redirection. This is fairly simple as you need to get the shortened URL and compare in database and return the original url.
app.get('/p/:url', async (req, res) => {
const short_url = req.params;
const originalUrl = await urlService.getOriginalUrl(short_url.url);
res.redirect(originalUrl);
});
Now you can write a simple service to get the original url
getOriginalUrl: async (short_url) => {
const originalUrl = await client.query(`select * from short_urls join url on short_urls.url_id = url.url_id where short_url like '%${short_url}%'`)
return originalUrl.rows[0].url;
}
You can build this project in a variety of ways. But, in this project, I have used a fairly simple approach. There will be many complexities as you start generalizing & scaling the application. One important this is DO NOT FORGET TO CREATE TABLES IN DATABASE . I am leaving that topic as a self-study. Which is fairly easier to create. I have used pgadmin for creating the tables.
And make sure to write the frontend and make the API call with axios or any such packages to the API endpoint.
Some useful articles on the web
https://codedamn.com/news/reactjs/how-to-connect-react-with-node-js
https://www.enjoyalgorithms.com/blog/design-a-url-shortening-service-like-tiny-url
https://www.educative.io/answers/what-is-node-cryptocreatehashalgorithm-options
Posted on March 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.