When choosing software, I always consider privacy as a make-or-break factor. One tool that I needed recently for a project was a URL shortener. There are many openly available apps out there, but using them is not the best option for me for two main reasons: I can lose control over my data and get frustrated with their limitations.

The alternative is to build my own app. Thankfully, n8n offers the needed capabilities to rapidly build web applications, without going through long coding nights. In this post, I will show you how I built my self-hosted URL shortener that can be customized and extended to fulfill different use cases. The final n8n workflow looks like this:

URL Shortener Workflow

The Idea

n8n hosts monthly community meetups, where members can present their workflows. At the April meetup, I was inspired by Jason's presentation of a newsletter sign-up workflow and his unique approach of using n8n as a full-fledged, highly extensible back-end. I decided to use that same approach to build a personal, self-hosted URL shortener that serves three functions:

  • Creating the short URL
  • Redirecting to the long URL
  • Generating statistics on a dashboard

Requirements

This project was created using n8n and nginx docker images, so before starting, I needed Docker and Docker-Compose installed. Also, I used Airtable as my database, which requires an Airtable account. To follow this tutorial, you need to have some basic experience with n8n and know how to configure nodes and expressions. Now let's get started!

Quickstart

To have a re-deployable infrastructure, I relied on a docker-compose file, in which I specified the necessary information about the docker containers.

First, I created an nginx.conf file to configure my nginx instance as a proxy. That way I could listen on port 80 of the localhost.

events {}
http {
    server {
        listen 80;
        location / {
            proxy_pass http://n8n;
            # enabling EventSource
            proxy_set_header Connection '';
            proxy_http_version 1.1;
            chunked_transfer_encoding off;
        }
        location /w/ {
            proxy_pass http://n8n/webhook/;
        }
    }
}
nginx.conf file content

Second, I configured the default n8n docker image to enable basic authentication. Also, I initiated the N8N_HOST and VUE_APP_URL_BASE_API environment variables with my preferred short URL base: n8n.ly.

version: '3.1'

services:

  n8n:
    image: n8nio/n8n
    container_name: n8n
    restart: always
    environment:
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=user
      - N8N_BASIC_AUTH_PASSWORD=secret
      - N8N_PROTOCOL=http
      - N8N_HOST=n8n.ly
      - N8N_PORT=80
      - VUE_APP_URL_BASE_API=http://n8n.ly
    volumes:
    # data persistence
      - ./n8nData:/home/node/.n8n
    networks:
      - shared-network
    command: /bin/sh -c "n8n start"

  nginx:
    image: nginx
    container_name: nginx
    restart: always
    ports:
      - 80:80
    volumes:
    # edited config
      - ./nginx.conf:/etc/nginx/nginx.conf
    networks:
      - shared-network

networks:
  shared-network:
docker-compose file content

Third, I registered the short URL base into my local DNS, by appending the next line to the /etc/hosts file:

127.0.0.1       n8n.ly
/etc/hosts last line

Now I could reference the n8n.ly domain locally and get redirected to my n8n instance.

The Workflow

The URL shortener is powered by a workflow containing 29 nodes. The workflow is split into three lines:

1. Short URL creation line with 12 nodes

In this line, I used a combination of Webhook node, Set node, and IF node to validate the request, then a combination of Crypto node, Airtable node, and Set node to create the short URL and append it to the database. Finally, I used the Set node to return the result.

2. Redirection line with 12 nodes

In this line, I used a combination of Webhook node, Set node, and IF node to receive the request, then the Airtable node, IF node, and Set node to retrieve the long URL. Finally, I used a Set node to prepare the result.

3. Dashboard line with 5 nodes

In this line, I handled the request using a Webhook node and used the Airtable node to retrieve all records. Then I converted those records into embedded statistics using a combination of Function node and Set node.

Now let’s go through each workflow line.

1. Short URL creation

I started by building the short URL creation line. In it, I generated a unique ID for the provided long URL and saved a record containing the generated ID, the long URL, the short URL, the number of clicks, and the hostname to a database. This is what the workflow line looks like:

Short URL creation workflow

Initially, I configured my Webhook node to accept requests on http://n8n.ly/w/sh and to return the last JSON object as HTTP response.

The first Webhook node configuration

Then, using the IF node, I checked the existence of the ?url query parameter. If the URL existed (true branch), I used the Set node to extract it. Else (false branch), I set an error value using a Set node.

After extracting the URL from the Webhook node output, I relied on hash functions to generate its unique ID. To calculate the URL HASH value, I used the Crypto node to generate a SHA256 hash.

Crypto node configuration

After calculating the HASH, I used a Set node to prepare the database record, which contains the Id, longUrl, and shortUrl fields.

After preparing the record, I verified if the generated ID already existed in my database. To do that, I used the Airtable node to retrieve the record by ID.

Airtable node (List) configuration

Then, I used an IF node to check whether the record already existed. If the record was found in the database (true branch), I used a Set node to set the shortUrl as the output. Else (false branch), I used a Set node to append a host and click fields to the prepared record.

Set node configuration

Then I used the Airtable node to append the record to the database and another Set node to set the created shortUrl as the output.

2. Redirection

Now with the redirection line, I aimed to redirect the user's web browser to the right long URL. To do that, I needed to retrieve the record using the provided ID, update the number of clicks for later statistics, and return a redirection page. The second workflow line looks like this:

Redirection workflow

Initially, I configured the Webhook node to listen on http://n8n.ly/w/go and to return the first JSON entry:

Webhook node configuration

Then I used the IF node to check the existence of the ?id query parameter. If the id query field existed (true branch), I used a Set node to extract it. Else (false branch), I returned an error page by using a Set node to set the error message.

Now that I had extracted the id parameter, I could verify if it existed in the database. If the record was not found, I used a Set node to create the correspondent error message. If the record existed (true branch), I used a Set node to increment the clicks counter, then an Airtable node to update the record in the database.

Airtable node configuration

After updating the database record, I used a Set node to prepare the HTML output. The secret here was embedding a piece of JavaScript code that would redirect the user to the long URL:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Redirection</title>

</head>
<body>
</body>

<script>
const load = function (){
    window.location.replace('{{$node\["Find by ID1"\].json.fields\["longUrl"\]}}');
};

window.onload = load;
</script>

</html>
Redirection page content

3. Dashboard

Now I have reached the most exciting part of the post: creating a dashboard. After designing my dashboard using Figma and implementing it using HTML and CSS, I could serve it to users. The workflow line for this step looks like this:

Dashboard workflow

I started by configuring a Webhook node to handle requests for http://n8n.ly/w/dashboard and return the first JSON entry to the user. The configuration for the Webhook node is similar to the previous one.

Then, I used an Airtable node to retrieve all the records from my database. Once I got the database result, I applied some magic to convert the record into statistics by using one of the most powerful nodes in n8n: the Function node, with the following code:

items = items.map(item => item.json.fields);
const totalLinks = items.length;
const totalClick = items.map(item => item.clicks).reduce((acc,val) => acc+=val);
const hostsMap = new Map();
const hosts = items.map(item => item.host);
hosts.forEach(host => {
	hostsMap.set(host,hostsMap.get(host)!==undefined?hostsMap.get(host)+1:1)
});

const totalHosts = [...hostsMap.keys()].length;

return [{
    json:{
        totalLinks,
        totalClick,
        totalHosts
    }
}];

After calculating the total links, total clicks, and total hosts numbers, I used a Set node to embed those statistics in my dashboard code:

Set node configuration

This is the full code in the expression:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Dashboard</title>
</head>
<style>
 *{
     padding: 0;
     margin: 0;
     border: none;
     box-sizing: border-box;
 }
 body{
     font-family: Roboto;
     font-style: normal;
 }
 .main{
     padding: 3rem 15rem;
     width: 70vw;
     min-height: 100vh;
     display: flex;
     flex-direction: column;
     margin: 0 auto;
 }
 .header{
     display: flex;
     flex-direction: row;
     justify-content: space-between;
     align-items: center;
     padding: 1rem 0.5rem;

 }
 .dashboard{
     display: grid;
     grid-template-rows: repeat(2, 1fr);
     grid-template-columns: repeat(2, 1fr);
     column-gap: 50px;
     row-gap: 50px;
     min-height: 70vh;
     min-width: calc(100vw-5rem);
 }
 .primary-text{
     color: #FF6D5A;
     font-family: Roboto;
     font-style: initial;
     font-weight: 500;
     font-size: 18px;
     line-height: 28px;
     /* center */
     display: flex;
     align-items: center;
     justify-content: center;
 }
 .main-box{
     min-height: 100%;
     min-width: 100%;
     background-color: #FF6D5A;
     grid-column: 1 / span 2;
     /* center */
     display: flex;
     flex-direction: rows;
     align-items: center;
     justify-content: center;
     /* font style */
     font-weight: bold;
     font-size: 96px;
     line-height: 169px;
     color: #F5F5F5;

 }
 .secondary-box{
     min-height: 100%;
     min-width: 100%;
     background-color: #384D5B;
     /* center */
     display: flex;
     flex-direction: row;
     align-items: center;
     justify-content: center;
     /* font style */
     font-weight: bold;
     font-size: 72px;
     line-height: 112px;
     color: #F5F5F5;
 }
 .info-text{
     position: absolute;
     align-self: flex-start;
     margin-top: 0.51rem;
     font-weight: 400;
     font-size: 16px;
     line-height: 21px;
     color: #F5F5F5;
 
 }
</style>

<body>
 
 <main class="main">
     <header class="header">
         <a href="https://n8n.io">
             <svg width="124px" height="28px" viewBox="0 0 124 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="nav-menu-(V1)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
                 --[SNIP n8n logo svg]--
             </svg>
         </a>
         <h4 class="primary-text">Dashboard</h4>
     </header>
 
 <section class="dashboard">
 
 	<div class="main-box">
         <h5 class="info-text">Total Clicks</h5>
        {{$node["Extract stats"].json["totalClick"]}}
     </div>
 
     <div class="secondary-box">
         <h5 class="info-text">Total Links</h5>
        {{$node["Extract stats"].json["totalLinks"]}}
     </div>
 
     <div class="secondary-box">
         <h5 class="info-text">Total Hosts</h5>
        {{$node["Extract stats"].json["totalHosts"]}}
     </div>
 
 </section>
 </main>
</body>
</html>
```
Dashboard HTML content

Now I could finally access the dashboard at http://n8n.ly/w/dashboard, which displays the total number of clicks, link, and hosts:

URL shortener dashboard

Next steps

In this post, I showed you how I used n8n to build a highly extensible, self-hosted URL shortener. I relied on a few core nodes like Webhook, IF, Set, Crypto, and Function to build three workflow lines that combined allowed me to create an app for my purposes. You can find the workflow here, feel free to use it and let me know what you think.

The workflow can be extended and customized to:

  • Use a different database node and make it 100% self-hosted
  • Offer a customized short URL where the user provides a slug instead of using an ID
  • Enrich the statistics returned in the dashboard to include more details

Start automating!

The best part is, you can start automating for free with n8n. The easiest way to get started is to download the desktop app, or sign up for a free n8n cloud trial. Thanks to n8n’s fair-code license, you can also self-host n8n for free.

Did you find this article useful or inspiring? Share it on Twitter or discuss it in the community forum 🧡