Build a URL shortener using React and Node/Express in Typescript
Naoki
Posted on January 22, 2023
Designing a URL shortener is frequently asked in technical interviews. In this article, I will describe how to design a URL shortener first, then I will walk you through how to implement the app as MVP(Minimum Viable Product) in Typescript.
System Design
Functional Requirements
A URL shortener generates a short URL for a long URL. In addition, when a user visits the short URL, they will be redirected to the long URL.
You might have some questions about the requirements that you might need to implement. The followings are some examples.
How long should a short URL be active? Will it ever expire?
How long should a short URL be? How many requests does the app handle?
Do we plan to track how many times a short URL is clicked?
Is a user allowed to generate a short URL by themselves?
In job interviews, you can always ask them to your interviewer and discuss what they expect. In this article, we will focus on MVP so that we will ignore optional requirements.
How to generate a random key?
How to generate a random key is not difficult. There are some ways to generate a random key such as using node:crypto and nanoid or you can create your function to generate a random key from [a-zA-Z0-9].
When is a random key for the short URL created?
The main point of building a URL shortener is WHEN a random key is created. I will introduce two ways to create random keys.
1. Generate a random key for the short URL AFTER a long URL is submitted
The idea is that after the api to create a short URL is called, a random key is generated. If the generated key is duplicated, re-generate a key until it has not been provided.
You might think that it is very rare to re-create the same key so we don't need to consider the duplication of keys. However, a short URL should be unique and as short as possible. Creating duplicated keys is more like to happen, unlike an ordinary hash key. In short, the more keys are generated, the slower generating a key is in this case.
2. Generate random keys BEFOREHAND and assign a key to a long URL
It is more efficient to generate random and unique keys beforehand. Then, when a short URL is provided, pick up the key and assign it to a long URL. This way can be faster to provide a short URL than the first way.
I will introduce both ways in the implementation section below.
Where to store a mapping of short URLs and long URLs
We will need a mapping of short URLs into long URLs to retrieve a long URL when a user visits the short URL.
We can store the mapping into a hashmap or DB(MongoDB or MySQL, etc). In general, it is better to store data in DB to keep data persistent because a hashmap will be reset when the server restarts. We focus on MVP so we will store data into hashmap at this time.
Implementation
Backend
We will implement two APIs, generate a short URL for a long URL, and redirect to a long URL when a user visits the short URL.
Generate a short URL
As I mentioned above, I will introduce two ways to generate a short URL. The difference between them is to create a unique key for a short URL BEFOREHAND or AFTER the API is called.
1. Generate a random key for the short URL AFTER a long URL is submitted
importexpress,{ErrorRequestHandler}from"express";importcorsfrom"cors";import{nanoid}from"nanoid";constport=4000;constdomain=`http://localhost:${port}`;constapp=express();app.use(cors());app.use(express.json());constshortURLs:{[key:string]:string}={};// short URL => long URLconstkeySet:Set<string>=newSet();app.post("/shorten-url",(res,req)=>{const{longUrl}=res.body||{};if (!longUrl)thrownewError("longUrl is necessary");if (!/^http(s){0,1}:\/\//.test(longUrl))thrownewError("Long URL should start with 'http://' or 'https://'");constkey=nanoid(5);if (keySet.has(key))thrownewError("key is duplicated");keySet.add(key);shortURLs[key]=longUrl;constshortUrl=`${domain}/${key}`;req.status(200).send({shortUrl});});consterrorHandler:ErrorRequestHandler=(err,_,res,__)=>{conststatus=err.status||404;res.status(status).send(err.message);};app.use(errorHandler);app.listen(port,()=>console.log(`Short URL app listening on port ${port}`));
Every time /shorten-url is called, nanoid generates a URL-friendly key. Then, the key and a long URL are stored in the mapping for URL redirection. And, the key is stored in a SET object to check if the generated key is duplicated. If the key is duplicated, the error will be thrown.
Express
Express is a Node web application that is designed to build APIs easily
CORS
CORS is a security feature that browsers prevent websites from requesting a server with different domains. The server can set domains that are allowed to access resource. app.use(cors()) means that CORS is not set up so that APIs in the server is open to being called from any websites on browsers. It is better to set up CORS in production.
app.use(express.json()) express.json() is a body parser that recognizes an incoming request object as a JSON object. As a side note, express.urlencoded() is a middleware that recognizes an incoming request object as a string or array.
2. Generate random keys BEFOREHAND and assign a key to a long URL
importexpress,{ErrorRequestHandler}from"express";importcorsfrom"cors";import{nanoid}from"nanoid";constport=4000;constdomain=`http://localhost:${port}`;constapp=express();app.use(cors());app.use(express.json());constkeySize=100;constshortURLs:{[key:string]:string}={};// short URL => long URLconstkeySet:Set<string>=newSet();while (keySet.size<keySize){keySet.add(nanoid(5));}constkeys=Array.from(keySet);app.post("/shorten-url",(res,req)=>{const{longUrl}=res.body||{};if (!longUrl)thrownewError("longUrl is necessary");if (!/^http(s){0,1}:\/\//.test(longUrl))thrownewError("Long URL should start with 'http://' or 'https://'");constkey=keys.pop();if (!key)thrownewError("the unique key ran out");shortURLs[key]=longUrl;constshortUrl=`${domain}/${key}`;req.status(200).send({shortUrl});});consterrorHandler:ErrorRequestHandler=(err,_,res,__)=>{conststatus=err.status||404;res.status(status).send(err.message);};app.use(errorHandler);app.listen(port,()=>console.log(`Short URL app listening on port ${port}`));
When the server starts, 100 unique keys are generated in the keys array. When /shorten-url API is called, a key from the array is assigned. If a key run out, the error will be thrown.
When a short URL is accessed, redirect to the long URL
importexpress,{ErrorRequestHandler}from"express";importcorsfrom"cors";import{nanoid}from"nanoid";constport=4000;constdomain=`http://localhost:${port}`;constapp=express();app.use(cors());app.use(express.json());constkeySize=100;constshortURLs:{[key:string]:string}={};// short URL => long URLconstkeySet:Set<string>=newSet();while (keySet.size<keySize){keySet.add(nanoid(5));}constkeys=Array.from(keySet);app.post("/shorten-url",(res,req)=>{const{longUrl}=res.body||{};if (!longUrl)thrownewError("longUrl is necessary");if (!/^http(s){0,1}:\/\//.test(longUrl))thrownewError("Long URL should start with 'http://' or 'https://'");constkey=keys.pop();if (!key)thrownewError("the unique key ran out");shortURLs[key]=longUrl;constshortUrl=`${domain}/${key}`;req.status(200).send({shortUrl});});// THIS IS THE NEW LINESapp.get("/:id",(res,req)=>{constlongUrl=shortURLs[res.params.id];if (!longUrl)thrownewError("the short url is wrong");req.redirect(longUrl);});consterrorHandler:ErrorRequestHandler=(err,_,res,__)=>{conststatus=err.status||404;res.status(status).send(err.message);};app.use(errorHandler);app.listen(port,()=>console.log(`Short URL app listening on port ${port}`));
Frontend
There are three elements we need in the frontend, an input box to type a long URL, a submit button to call an API generating a short URL, and text showing a short URL.
When a submit button is hit, API to generate a short URL is called. And, show the short URL in the response. The following code is an MVP for the frontend.
Let's style components by using Mantine and write tests for the backend and frontend using Jest, React Testing Library, and Cypress. The followings are the final UI.
The latest code is in the GitHub Repository below. I don't explain tests today but the code in GitHub is fully tested with Jest, React Testing Library, and Cypress. And, it is refactored to make the code clean and testable. Please check this out!