Make a URL shortener in Node.js!
Tags: node, javascript, tutorial
Published: 2023-02-14 16:00:00

## Intro
I will show you how to create a simple and efficient basic URL shortener in Node.js with Express.js - it is a great personal project for learning with the bonus of being easily adaptable and expandable.

_Full code: https://github.com/kacperturon/urlshortener_
## Prerequisites: 
Create new Node project `npm init -y` install Express.js `npm i express` add to package.json to scripts a 'start' script `start: "node index.js"`

_Optionally:_
- Add eslint for linting: 
`npm i -D eslint` 
`npx init eslint`
- If using VS Code add extension https://tinyurl.com/vs-rest-client for testing purposes

---

## 1. Set up the server and 2 endpoints

```javascript
app.post('/shorten', (req, res) => { 
  const { url, key } = req.body;
  ...
}
app.get('/:hash', (req, res) => {
  const { hash } = req.params;
  ...
}
app.listen(port, () => console.log(`Server: URL shortener started on port ${port}`));
```

---

## 2. Create helpers

- Generate a hash of a specified length:
```javascript
const getRandomHash = (len) => crypto.createHash('shake256', { outputLength: len }).update(crypto.randomBytes(20).toString('hex')).digest('hex');
```
- Calculate amount of possible combinations
26 [a-z] + 10 [0-9] ^ len + 1 (getRandomHash is in bytes so we add 1):
```javascript
const possibleHashCombinations = (len) => (26 + 10) ** (len + 1);
```

---

## 3. Design data structure to keep track of the shortened URLs

```javascript
const hashes = new Set();
const urlHashes = {};
```
Example data:
```JSON
Set(5) { 'google', 'a3', '76', '60', 'cc' }
{ 'http://google.com': [ 'google', 'a3', '76', '60', 'cc' ],
  'https://dev.to/: [ 'ge', 'dev' ] }
```
_alternative approach: instead of having the key be the URL have the key be the hash, this will greatly impact our memory usage but also will greatly increase performance._

---

## 4. Design two endpoints 

- **POST** `/shorten` - shorten the received URL given a key or by generating a random hash

Generate hashes until one is unique or execution has timed out after 10s:
```javascript
setTimeout(() => { halt = true; }, 1000 * 10);
do {
    hash = getRandomHash(hashLength);
    if (halt) return res.sendStatus(500);
} while (hashes.has(hash) || hash === null);
hashes.add(hash);
urlHashes[url] = urlHashes[url] ? [...urlHashes[url], hash] : [hash];
```
_alternative approach: pre-generate hashes on server launch and immediately return the first free hash._

If all combinations are used up increase the `hashLength` and recalculate:
```javascript
combinationsAvailable -= 1;
if (combinationsAvailable === 0) {
    hashLength += 1;
    combinationsAvailable = possibleHashCombinations(hashLength);
}
return res.send(`${domain}/${hash}`);
```
- **GET** `/:hash` - retrieve shortened URL and redirect
```javascript
  const url = Object.keys(urlHashes).find((u) => urlHashes[u].includes(hash));
  return res.redirect(url);
```

---

## 5. Deal with edge-cases

## `/shorten`
 - `HTTP 403` - no URL provided
 - `HTTP 403` - `key` provided already exists
 - `HTTP 403` - `hash` or `key` already exists
 - `HTTP 500` - timed out while creating a unique `hash`

## `/:hash`
 - `HTTP 404` - the URL for the hash does not exist

---

## 6. Test the endpoints

```JSON
### POST with a specified key
POST http://localhost:3000/shorten
content-type: application/json

{
    "url": "http://google.com",
    "key": "google"
}
```
![POST with specified key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9g7wsf63b2mzt2u9aj5h.png)

```JSON
### POST without a key
POST http://localhost:3000/shorten
content-type: application/json

{
    "url": "http://google.com"
}
```
![POST without a key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t1rh5p0ynepcos1ttkoa.png)

```JSON
### GET existing URL
GET http://localhost:3000/google
```
![GET existing URL - partial response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ztrdpfimrxszcu6hgxnv.png)
```JSON
### GET non-existent URL
GET http://localhost:3000/googlenonexistent
```

![GET non-existent URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/std5r4ja5frqv31xqz25.png)







---

## 7. Expandability ideas:
- Allow multiple URLs to be shortened at once
- Combine it with a Discord bot to pass users shortened URLs in chat
- On stopping the server create a backup of hashes and URLs and restore on launch
- Add full testing suite i.e. JEST or MOCHA

---

## Conclusion
We went through the steps of creating a preliminary version of the URL shortener service, how to approach the design, split the work into endpoints and decide on a data structure, creating a great starting point for more specialised use cases.

---
---

Please let me know what you think of this article since this is my first one! Is it too simple? too short? valuable? good for new engineers? 

What else would be interesting in the future:
- expanding this project?
- creating clones of i.e. Leetcode or Youtube or Twitter..
- focusing on actual Leetcode problems
- improvement ideas for programming React, microservices?
- system design work?

**Any and all feedback is appreciated in these early stages of my article writing!**