Build a URL shortener using React and Node/Express in Typescript

nk18chi

Naoki

Posted on January 22, 2023

Build a URL shortener using React and Node/Express in Typescript

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


import express, { ErrorRequestHandler } from "express";
import cors from "cors";
import { nanoid } from "nanoid";

const port = 4000;
const domain = `http://localhost:${port}`;
const app = express();

app.use(cors());
app.use(express.json());

const shortURLs: { [key: string]: string } = {}; // short URL => long URL
const keySet: Set<string> = new Set();

app.post("/shorten-url", (res, req) => {
  const { longUrl } = res.body || {};
  if (!longUrl) throw new Error("longUrl is necessary");
  if (!/^http(s){0,1}:\/\//.test(longUrl)) throw new Error("Long URL should start with 'http://' or 'https://'");

  const key = nanoid(5);
  if (keySet.has(key)) throw new Error("key is duplicated");
  keySet.add(key);
  shortURLs[key] = longUrl;
  const shortUrl = `${domain}/${key}`;
  req.status(200).send({ shortUrl });
});

const errorHandler: ErrorRequestHandler = (err, _, res, __) => {
  const status = err.status || 404;
  res.status(status).send(err.message);
};
app.use(errorHandler);

app.listen(port, () => console.log(`Short URL app listening on port ${port}`));


Enter fullscreen mode Exit fullscreen mode

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



import express, { ErrorRequestHandler } from "express";
import cors from "cors";
import { nanoid } from "nanoid";

const port = 4000;
const domain = `http://localhost:${port}`;
const app = express();

app.use(cors());
app.use(express.json());

const keySize = 100;
const shortURLs: { [key: string]: string } = {}; // short URL => long URL
const keySet: Set<string> = new Set();
while (keySet.size < keySize) {
  keySet.add(nanoid(5));
}
const keys = Array.from(keySet);

app.post("/shorten-url", (res, req) => {
  const { longUrl } = res.body || {};
  if (!longUrl) throw new Error("longUrl is necessary");
  if (!/^http(s){0,1}:\/\//.test(longUrl)) throw new Error("Long URL should start with 'http://' or 'https://'");

  const key = keys.pop();
  if (!key) throw new Error("the unique key ran out");
  shortURLs[key] = longUrl;
  const shortUrl = `${domain}/${key}`;
  req.status(200).send({ shortUrl });
});

const errorHandler: ErrorRequestHandler = (err, _, res, __) => {
  const status = err.status || 404;
  res.status(status).send(err.message);
};
app.use(errorHandler);

app.listen(port, () => console.log(`Short URL app listening on port ${port}`));


Enter fullscreen mode Exit fullscreen mode

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



import express, { ErrorRequestHandler } from "express";
import cors from "cors";
import { nanoid } from "nanoid";

const port = 4000;
const domain = `http://localhost:${port}`;
const app = express();

app.use(cors());
app.use(express.json());

const keySize = 100;
const shortURLs: { [key: string]: string } = {}; // short URL => long URL
const keySet: Set<string> = new Set();
while (keySet.size < keySize) {
  keySet.add(nanoid(5));
}
const keys = Array.from(keySet);

app.post("/shorten-url", (res, req) => {
  const { longUrl } = res.body || {};
  if (!longUrl) throw new Error("longUrl is necessary");
  if (!/^http(s){0,1}:\/\//.test(longUrl)) throw new Error("Long URL should start with 'http://' or 'https://'");

  const key = keys.pop();
  if (!key) throw new Error("the unique key ran out");
  shortURLs[key] = longUrl;
  const shortUrl = `${domain}/${key}`;
  req.status(200).send({ shortUrl });
});

// THIS IS THE NEW LINES
app.get("/:id", (res, req) => {
  const longUrl = shortURLs[res.params.id];
  if (!longUrl) throw new Error("the short url is wrong");
  req.redirect(longUrl);
});

const errorHandler: ErrorRequestHandler = (err, _, res, __) => {
  const status = err.status || 404;
  res.status(status).send(err.message);
};
app.use(errorHandler);

app.listen(port, () => console.log(`Short URL app listening on port ${port}`));


Enter fullscreen mode Exit fullscreen mode

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.



import { useCallback, useState } from "react";
import "./App.css";

interface useShortUrlProps {
  url: string;
}

const useShortUrl = ({ url }: useShortUrlProps) => {
  const [shortUrl, setShortUrl] = useState<string>("");
  const [loading, setLoading] = useState<boolean>(false);
  const [message, setMessage] = useState<string>("");
  const handleShortUrl = useCallback(async () => {
    setLoading(true);
    const res = await fetch("http://localhost:4000/shorten-url", {
      method: "POST",
      body: JSON.stringify({ longUrl: url }),
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
    });
    if (!res.ok) {
      setMessage(await res.text());
      return;
    }
    const { shortUrl } = (await res.json()) || {};
    if (!shortUrl) {
      setMessage("something went wrong.");
      return;
    }
    setLoading(false);
    setShortUrl(shortUrl);
  }, [url]);
  return { shortUrl, loading, message, handleShortUrl };
};

const App = () => {
  const [url, setUrl] = useState<string>("");
  const { shortUrl, loading, message, handleShortUrl } = useShortUrl({ url });
  return (
    <div className='App'>
      <div>
        <input
          type='text'
          onChange={(e) => {
            setUrl(e.target.value);
          }}
        />
        <button onClick={handleShortUrl}>Shorten</button>
      </div>
      <div>
        {loading ? (
          <p>loading</p>
        ) : (
          <>
            {shortUrl && (
              <p>
                shortUrl:{" "}
                <a href={shortUrl} target='_blank' rel='noreferrer'>
                  {shortUrl}
                </a>
              </p>
            )}
            {message && <p>message: {message}</p>}
          </>
        )}
      </div>
    </div>
  );
};

export default App;


Enter fullscreen mode Exit fullscreen mode

The output of the above code is following.

url shortener demo

Brush up code

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.

url shortener final 1
url shortener final 2
url shortener final 3

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!

GitHub logo nk18chi / URL-Shortener

a simple URL shortener






Hope this article is helpful to someone. Enjoy coding!

💖 💪 🙅 🚩
nk18chi
Naoki

Posted on January 22, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related