Build a URL Shortener with Node.js

Build a URL shortener API with Express and SQLite that tracks clicks.

Introduction

30 Second Summary

Every time you share a link online, you rely on the same simple idea: take a long, ugly URL and turn it into something short and memorable. Services like Bitly do this billions of times a day.

In this project, you will build your own URL shortener service from scratch using Node.js and Express. You'll create an API that accepts long URLs, generates short codes, stores them in a database, and redirects visitors to the original destination.

What You'll Build

A fully functional URL shortener running on your local machine with a REST API, a SQLite database for persistence, and a simple browser-based form.

By the end of this project, you'll have:

  • A Node.js server with Express that handles URL shortening and HTTP redirection.
  • A SQLite database that persists URL mappings across server restarts and tracks click counts.
  • A browser-based HTML form that lets you shorten URLs without touching the terminal.
  • Secret Mission: Add custom aliases so users can choose their own short codes.

Are there any prerequisites?

You should be comfortable with basic JavaScript concepts like variables, functions, and objects. No prior experience with Node.js, Express, or databases is required.

Before We Start

Before We Start...

Set Up Your Development Environment

Before building the URL shortener, you need a dedicated project folder with the right packages installed.

In this step, you'll create a new Node.js project and install three packages: Express for the web server, better-sqlite3 for the database, and nanoid for generating short codes.

In this step, get ready to:
  • Confirm Node.js is ready on your machine.
  • Create and initialize the project folder.
  • Install the project dependencies.
Confirm Node.js is ready

Node.js is the JavaScript runtime that powers your server. Let's confirm it's working before building anything on top of it.

  • Switch back to your terminal and confirm Node.js is installed by running this command:
node --version

You should see a version number like v22.x.x or higher printed in the terminal.

Don't see a version number?

If you see "command not found" or an error, Node.js is not installed. Download and install it from nodejs.org. Choose the LTS version, run the installer, then close and reopen your terminal before trying again.

help me install Node.js on my computer

Create the project folder and initialize it

Every Node.js project starts with a folder and a package.json file. This file tracks your project's name, version, and which packages it depends on.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir url-shortener
cd url-shortener
  • Confirm the folder exists by running:
ls

The folder will be empty for now. That's expected.

  • Initialize a new Node.js project by running:
npm init -y

What does this command do?

The npm init -y command creates a package.json file with default values. The -y flag skips all the setup questions and accepts the defaults.

You should see output confirming the file was created.

Now you need to configure package.json for this project. The default file needs a few changes: enabling ES modules, setting the entry point to server.js, and adding a start script.

  • Open the url-shortener folder in VS Code. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the url-shortener folder, and click Open.
  • In the Explorer sidebar on the left, click package.json to open it.
  • Select all the text in package.json (Cmd+A on macOS or Ctrl+A on Windows) and replace it with the following:
{
  "name": "url-shortener",
  "version": "1.0.0",
  "type": "module",
  "description": "A simple URL shortener built with Node.js, Express, and SQLite",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "better-sqlite3": "12.9.0",
    "express": "5.2.1",
    "nanoid": "5.1.11"
  }
}

What does this code do?

The "type": "module" line tells Node.js to use modern ES module syntax (import/export) instead of the older require() style. This is required because nanoid 5.x only works with ES modules.

The dependencies section lists the three packages your project needs, each pinned to a specific version. Running npm install next will download these exact versions.

  • Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

Your package.json should look exactly like this:

{
  "name": "url-shortener",
  "version": "1.0.0",
  "type": "module",
  "description": "A simple URL shortener built with Node.js, Express, and SQLite",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "better-sqlite3": "12.9.0",
    "express": "5.2.1",
    "nanoid": "5.1.11"
  }
}
Install the project dependencies

Your package.json lists three dependencies, but they haven't been downloaded yet. The npm install command reads your dependencies and downloads them into a node_modules folder.

  • In the VS Code terminal (open it with Ctrl+` or Terminal > New Terminal from the top menu), install the dependencies by running:
npm install

What does this install?

  • express 5.2.1 is the web framework that handles incoming HTTP requests and sends responses.
  • better-sqlite3 12.9.0 is a fast SQLite driver that stores data in a local file. No database server needed.
  • nanoid 5.1.11 generates short, URL-friendly random IDs. This is what creates the short codes for your URLs.
  • Confirm the install succeeded by running:
ls

You should see node_modules and package-lock.json listed alongside your package.json.

Seeing errors during install?

The better-sqlite3 package includes native code that needs compiling. On Windows, you may see errors about missing build tools. If this happens, try running your terminal as Administrator and install Windows Build Tools first.

If you see a different error, check that you're inside the url-shortener folder (run pwd to check) and that your package.json was saved correctly.

help me fix npm install errors

Your project structure is in place and all three packages are installed. Next up, you'll create the database layer that stores URL mappings.

Create the Database Layer

Your URL shortener needs somewhere to store the mappings between short codes and original URLs. Without a database, every shortened link would disappear the moment your server restarts.

In this step, you'll create a SQLite database layer using better-sqlite3. SQLite stores everything in a single file on disk, so there's no separate database server to install or configure.

In this step, get ready to:
  • Initialize a SQLite database with a urls table.
  • Create helper functions for saving, retrieving, and updating URL records.
Create the database file and table

Every URL mapping needs to be stored with its short code, the original URL, a timestamp, and a click counter. You'll define this structure as a SQL table.

  • Create a new file called database.js in your url-shortener folder by running this command:
touch database.js
  • Confirm the file exists by running:
ls

You should see database.js, package.json, and node_modules in the output.

  • Open database.js in VS Code by clicking on it in the Explorer sidebar.
  • Add the database initialization code by pasting the following into database.js:
import Database from 'better-sqlite3';

const db = new Database('urls.db');

db.pragma('journal_mode = WAL');

db.exec(`
  CREATE TABLE IF NOT EXISTS urls (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    short_code TEXT UNIQUE NOT NULL,
    original_url TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now')),
    clicks INTEGER DEFAULT 0
  )
`);

What does this code do?

The first line imports the better-sqlite3 library using ES module syntax. Then new Database('urls.db') creates (or opens) a file called urls.db in your project folder.

The WAL pragma enables Write-Ahead Logging, which improves read and write performance.

The CREATE TABLE IF NOT EXISTS statement defines the shape of your data. Each row will hold a unique short code, the original URL it points to, a creation timestamp, and a click counter starting at zero.

  • Save database.js.
  • Test the initialization by running this command in your terminal:
node database.js
  • Run ls to confirm urls.db was created:
ls

You should see urls.db in the file listing. This is your SQLite database file.

Don't see urls.db?

  • Make sure you saved database.js before running node database.js.
  • Check that your terminal is inside the url-shortener folder. Run pwd (macOS/Linux) or cd (Windows) to confirm.
  • If you see an error about import syntax, confirm that your package.json contains "type": "module".

Still stuck? Help me debug why node database.js isn't creating my urls.db file.

Add helper functions for database operations

Your server will need three operations: saving a new URL mapping, looking one up by short code, and incrementing the click counter. You'll export each as a named function so the server can import them.

  • Add the following helper functions below the db.exec(...) block in database.js:
export function saveUrl(shortCode, originalUrl) {
  const stmt = db.prepare(
    'INSERT INTO urls (short_code, original_url) VALUES (?, ?)'
  );
  stmt.run(shortCode, originalUrl);
}

export function getUrl(shortCode) {
  const stmt = db.prepare('SELECT * FROM urls WHERE short_code = ?');
  return stmt.get(shortCode);
}

export function incrementClicks(shortCode) {
  const stmt = db.prepare(
    'UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?'
  );
  stmt.run(shortCode);
}

What do these functions do?

  • saveUrl inserts a new row with the short code and original URL. The ? placeholders prevent SQL injection by safely escaping user input.
  • getUrl looks up a single row by its short code. It returns the full record (including click count) or undefined if no match exists.
  • incrementClicks bumps the click counter by one each time someone visits the short URL.
  • Save database.js.
  • Run the file again to confirm there are no syntax errors:
node database.js

The command should complete with no output and no errors. This confirms your database layer is ready for the server to use.

Seeing a syntax error?

  • Check that each function uses export function at the start. Missing the export keyword won't cause an error now, but will break imports later.
  • Make sure you have matching parentheses and backticks in the SQL strings.
  • Confirm the helper functions are placed after the db.exec(...) closing ;, not inside it.

Need help? Help me fix a syntax error in my database.js helper functions.

✔️ Awesome, I've got everything!

Great. Double check you've saved database.js and move on to the next step.

ⓧ I'd like to double check the full code

Here's the complete database.js file:

import Database from 'better-sqlite3';

const db = new Database('urls.db');

db.pragma('journal_mode = WAL');

db.exec(`
  CREATE TABLE IF NOT EXISTS urls (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    short_code TEXT UNIQUE NOT NULL,
    original_url TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now')),
    clicks INTEGER DEFAULT 0
  )
`);

export function saveUrl(shortCode, originalUrl) {
  const stmt = db.prepare(
    'INSERT INTO urls (short_code, original_url) VALUES (?, ?)'
  );
  stmt.run(shortCode, originalUrl);
}

export function getUrl(shortCode) {
  const stmt = db.prepare('SELECT * FROM urls WHERE short_code = ?');
  return stmt.get(shortCode);
}

export function incrementClicks(shortCode) {
  const stmt = db.prepare(
    'UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?'
  );
  stmt.run(shortCode);
}

Your database layer is complete. Next up, you'll build the Express server that uses these functions to handle incoming requests.

Build the Express Server and API Routes

Your database layer can store URLs, look them up, and track clicks. Now you need a way for the outside world to talk to it.

In this step, you'll build the Express server that listens for HTTP requests and routes them to your database functions. You'll create three API endpoints: one to shorten URLs, one to redirect visitors, and one to check click statistics.

In this step, get ready to:
  • Create the Express server with middleware for JSON parsing and static files.
  • Build a POST endpoint that shortens URLs and stores them in your database.
  • Add GET routes for redirecting short links and checking analytics.
Create the server file and shorten endpoint

Express is a web framework that handles incoming HTTP requests and sends responses. Your server will be the single entry point that connects the browser to your database layer.

  • Create a new file called server.js in the url-shortener folder.
  • Add the server setup code by pasting the following into server.js:
import express from 'express';
import { nanoid } from 'nanoid';
import { saveUrl, getUrl, incrementClicks } from './database.js';

const app = express();
const PORT = 3000;

// Parse incoming JSON request bodies
app.use(express.json());

// Serve static files from the 'public' folder
app.use(express.static('public'));

app.listen(PORT, () => {
  console.log(`URL Shortener running at http://localhost:${PORT}`);
});

What does this code do?

  • The three import lines pull in Express (the web framework), nanoid (for generating short codes), and your database helper functions.
  • express.json() tells Express to automatically parse JSON data from incoming requests so you can read it in your route handlers.
  • express.static('public') tells Express to serve any files inside a public folder directly to the browser. You'll create this folder in the next step.
  • app.listen(PORT) starts the server on port 3000 and prints a confirmation message.
  • Save server.js.
  • Test that your server starts by running this command in the terminal:
node server.js
  • You should see URL Shortener running at http://localhost:3000 in your terminal.
  • Press Ctrl+C to stop the server. You'll restart it after adding routes.

Server won't start?

  • If you see Cannot find module errors, make sure you ran npm install in the previous step. Your node_modules folder must exist.
  • If you see a syntax error, check that your package.json contains "type": "module". Without it, Node.js won't understand the import syntax.
  • Help me debug why my server won't start.

Now you'll add the first API route. This is the endpoint that receives a long URL and returns a shortened version.

  • In server.js, find the line that says app.listen(PORT, () => {.
  • Add the POST route by pasting the following code above that line:
// Shorten a URL
app.post('/api/shorten', (req, res) => {
  const { url } = req.body;

  if (!url) {
    return res.status(400).json({ error: 'URL is required' });
  }

  try {
    new URL(url);
  } catch {
    return res.status(400).json({ error: 'Invalid URL format' });
  }

  const shortCode = nanoid(8);

  try {
    saveUrl(shortCode, url);
  } catch (error) {
    return res.status(500).json({ error: 'Failed to save URL' });
  }

  const shortUrl = `http://localhost:${PORT}/${shortCode}`;
  res.status(201).json({ shortUrl, shortCode, originalUrl: url });
});

What does this route do?

This route handles POST requests to /api/shorten. Here's the flow:

  • It extracts the url from the request body, then checks that it exists and is a valid URL format using the built-in URL constructor.
  • nanoid(8) generates a random 8-character code using URL-safe characters (letters, numbers, hyphens, underscores).
  • It saves the mapping to your database using the saveUrl() function you created in the last step, then responds with the full short URL.
  • Save server.js and restart the server by running:
node server.js
  • You should see the same URL Shortener running at http://localhost:3000 message. This confirms your POST route has no syntax errors.
  • Press Ctrl+C to stop the server again.

Seeing an error on restart?

  • Check that the POST route code is placed above app.listen, not inside or below it.
  • Make sure you have a closing }); at the end of the route handler. Missing brackets cause syntax errors.
  • Help me fix a syntax error in my Express route.
Add the stats route

URL shorteners track how many times each link gets clicked. This endpoint lets users check the analytics for any short code.

  • In server.js, find the app.listen(PORT, () => { line again.
  • Add the stats route by pasting the following code above that line:
// Get stats for a short URL
app.get('/api/stats/:code', (req, res) => {
  const { code } = req.params;
  const urlRecord = getUrl(code);

  if (!urlRecord) {
    return res.status(404).json({ error: 'Short URL not found' });
  }

  res.json(urlRecord);
});

What does this route do?

  • :code in the path is a route parameter. Express captures whatever appears after /api/stats/ and makes it available as req.params.code.
  • It uses your getUrl() function to look up the record. If found, it returns the full record (including click count). If not, it returns a 404 error.
  • Save server.js.

You won't see a change yet. The next route (the redirect) completes the picture and you'll test everything together.

Add the redirect route

This is the core of your URL shortener. When someone visits a short link, this route looks up the original URL and redirects the browser there.

  • In server.js, find the app.listen(PORT, () => { line.
  • Add the redirect route by pasting the following code above that line:
// Redirect to original URL
app.get('/:code', (req, res) => {
  const { code } = req.params;
  const urlRecord = getUrl(code);

  if (!urlRecord) {
    return res.status(404).json({ error: 'Short URL not found' });
  }

  incrementClicks(code);
  res.redirect(302, urlRecord.original_url);
});

What does this route do?

  • /:code matches any path like /abc123xy. This is a catch-all pattern, which is why it must be the last route defined.
  • incrementClicks(code) bumps the click counter in your database every time someone uses the short link.
  • res.redirect(302, ...) sends an HTTP 302 response that tells the browser to go to the original URL. This is the same mechanism Bitly and TinyURL use.

Why does route order matter?

Express matches routes in the order they are defined. The /:code pattern is greedy. It would match /api/stats/abc123 if it came first, treating api as the code.

By placing the more specific /api/stats/:code route above the catch-all /:code route, Express checks the specific path first and only falls through to the redirect if nothing else matches.

  • Save server.js and start the server by running:
node server.js
  • You should see URL Shortener running at http://localhost:3000 in your terminal. All three routes are now active.
  • Press Ctrl+C to stop the server for now. You'll start it again once the frontend is ready.

Server crashes on startup?

  • Make sure each route ends with }); and that the routes appear between the app.use() middleware lines and the app.listen() call at the bottom.
  • If you see EADDRINUSE, it means the previous server is still running. Press Ctrl+C in the terminal first, then try again.
  • Help me debug my Express server routes.

✔️ Awesome, I've got everything!

Great. Double check you've saved server.js and that it starts without errors.

ⓧ I'd like to double check the full code

Here's the complete server.js file. Compare yours against this:

import express from 'express';
import { nanoid } from 'nanoid';
import { saveUrl, getUrl, incrementClicks } from './database.js';

const app = express();
const PORT = 3000;

app.use(express.json());
app.use(express.static('public'));

// Shorten a URL
app.post('/api/shorten', (req, res) => {
  const { url } = req.body;

  if (!url) {
    return res.status(400).json({ error: 'URL is required' });
  }

  try {
    new URL(url);
  } catch {
    return res.status(400).json({ error: 'Invalid URL format' });
  }

  const shortCode = nanoid(8);

  try {
    saveUrl(shortCode, url);
  } catch (error) {
    return res.status(500).json({ error: 'Failed to save URL' });
  }

  const shortUrl = `http://localhost:${PORT}/${shortCode}`;
  res.status(201).json({ shortUrl, shortCode, originalUrl: url });
});

// Get stats for a short URL
app.get('/api/stats/:code', (req, res) => {
  const { code } = req.params;
  const urlRecord = getUrl(code);

  if (!urlRecord) {
    return res.status(404).json({ error: 'Short URL not found' });
  }

  res.json(urlRecord);
});

// Redirect to original URL
app.get('/:code', (req, res) => {
  const { code } = req.params;
  const urlRecord = getUrl(code);

  if (!urlRecord) {
    return res.status(404).json({ error: 'Short URL not found' });
  }

  incrementClicks(code);
  res.redirect(302, urlRecord.original_url);
});

app.listen(PORT, () => {
  console.log(`URL Shortener running at http://localhost:${PORT}`);
});

Your API is ready to shorten URLs, redirect visitors, and track clicks. Next up, you'll build a simple web page so you can test the whole flow from a browser instead of just code.

Add the Frontend Form

Your server can shorten URLs, track clicks, and redirect visitors. But right now, the only way to use it is by crafting raw HTTP requests manually.

In this step, you'll build a simple web page with a form that lets anyone paste a URL and get a short link back instantly. The page talks to your API using JavaScript's fetch() function.

In this step, get ready to:
  • Create the public folder and HTML form with styling.
  • Add JavaScript to connect the form to your API.
Create the HTML form and styling

Express is already configured to serve files from a public folder (you set that up in server.js with express.static('public')). You just need to create that folder and put an HTML file inside it.

  • Create the public folder by running this command in your terminal:
mkdir public
  • In VS Code, right-click the public folder in the Explorer sidebar and select New File....
  • Name the file index.html and press Enter.
  • Copy the following HTML and CSS into public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>URL Shortener</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      max-width: 600px;
      margin: 50px auto;
      padding: 20px;
      background: #f5f5f5;
    }
    h1 { color: #333; }
    form {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    input[type="url"] {
      width: 100%;
      padding: 12px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 16px;
      box-sizing: border-box;
      margin-bottom: 12px;
    }
    button {
      background: #007bff;
      color: white;
      border: none;
      padding: 12px 24px;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
    }
    button:hover { background: #0056b3; }
    #result {
      margin-top: 20px;
      padding: 16px;
      background: #e8f5e9;
      border-radius: 4px;
      display: none;
    }
    #result a {
      color: #007bff;
      font-weight: bold;
      word-break: break-all;
    }
    .error {
      background: #ffebee !important;
      color: #c62828;
    }
  </style>
</head>
<body>
  <h1>URL Shortener</h1>
  <form id="shorten-form">
    <input type="url" id="url-input" placeholder="Paste a long URL here..." required>
    <button type="submit">Shorten</button>
  </form>
  <div id="result"></div>

  <script>
  </script>
</body>
</html>

What does this HTML do?

  • The <form> has an id="shorten-form" so JavaScript can listen for submissions.
  • The <input type="url"> uses the browser's built-in URL validation (the required attribute prevents empty submissions).
  • The <div id="result"> starts hidden (display: none) and only appears after the form is submitted.
  • The .error class changes the result box to red when something goes wrong.
  • Save public/index.html.
  • Confirm the file exists by running:
ls public

You should see index.html in the output.

Don't see index.html listed?

  • Make sure you created the file inside the public folder, not in the project root.
  • Check that you saved the file after pasting the code.
  • Verify you're running ls public from the url-shortener folder, not from inside public itself.

help me find where my index.html file ended up

Add the form submission script

The HTML form looks nice, but it doesn't do anything yet. You need JavaScript to intercept the form submission, send the URL to your API, and display the result.

  • In public/index.html, find the empty <script> tags near the bottom of the file.
  • Paste the following JavaScript between the opening <script> and closing </script> tags:
    const form = document.getElementById('shorten-form');
    const urlInput = document.getElementById('url-input');
    const result = document.getElementById('result');

    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      const url = urlInput.value;

      try {
        const response = await fetch('/api/shorten', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ url })
        });

        const data = await response.json();

        if (response.ok) {
          result.className = '';
          result.innerHTML = `
            Short URL: <a href="${data.shortUrl}" target="_blank">${data.shortUrl}</a>
          `;
        } else {
          result.className = 'error';
          result.innerHTML = data.error;
        }
        result.style.display = 'block';
      } catch (error) {
        result.className = 'error';
        result.innerHTML = 'Something went wrong. Please try again.';
        result.style.display = 'block';
      }
    });

What does this script do?

  • e.preventDefault() stops the form from refreshing the page on submit.
  • fetch('/api/shorten', ...) sends the URL to your Express API as a JSON POST request.
  • If the response is successful, it displays the short URL as a clickable link that opens in a new tab.
  • If the API returns an error (invalid URL, server failure), the result box turns red and shows the error message.
  • Save public/index.html.

You won't see a change yet. The next step starts the server and tests everything end to end.

Worried you pasted in the wrong spot?

The JavaScript should sit between <script> and </script>, which are right after <div id="result"></div>. If you accidentally pasted it inside the <style> block or outside the <body>, the form won't work when you test it in the next step.

help me check my script placement in index.html

✔️ Awesome, I've got everything!

Great. Make sure you've saved public/index.html before moving on.

ⓧ I'd like to double check the full code

Here's the complete public/index.html file. Make sure yours matches exactly:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>URL Shortener</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      max-width: 600px;
      margin: 50px auto;
      padding: 20px;
      background: #f5f5f5;
    }
    h1 { color: #333; }
    form {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    input[type="url"] {
      width: 100%;
      padding: 12px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 16px;
      box-sizing: border-box;
      margin-bottom: 12px;
    }
    button {
      background: #007bff;
      color: white;
      border: none;
      padding: 12px 24px;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
    }
    button:hover { background: #0056b3; }
    #result {
      margin-top: 20px;
      padding: 16px;
      background: #e8f5e9;
      border-radius: 4px;
      display: none;
    }
    #result a {
      color: #007bff;
      font-weight: bold;
      word-break: break-all;
    }
    .error {
      background: #ffebee !important;
      color: #c62828;
    }
  </style>
</head>
<body>
  <h1>URL Shortener</h1>
  <form id="shorten-form">
    <input type="url" id="url-input" placeholder="Paste a long URL here..." required>
    <button type="submit">Shorten</button>
  </form>
  <div id="result"></div>

  <script>
    const form = document.getElementById('shorten-form');
    const urlInput = document.getElementById('url-input');
    const result = document.getElementById('result');

    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      const url = urlInput.value;

      try {
        const response = await fetch('/api/shorten', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ url })
        });

        const data = await response.json();

        if (response.ok) {
          result.className = '';
          result.innerHTML = `
            Short URL: <a href="${data.shortUrl}" target="_blank">${data.shortUrl}</a>
          `;
        } else {
          result.className = 'error';
          result.innerHTML = data.error;
        }
        result.style.display = 'block';
      } catch (error) {
        result.className = 'error';
        result.innerHTML = 'Something went wrong. Please try again.';
        result.style.display = 'block';
      }
    });
  </script>
</body>
</html>

Your frontend is ready to go. In the next step, you'll start the server and test the full flow from pasting a URL to clicking the short link.

Test Your URL Shortener

Your frontend form is ready and all three API routes are in place. Now it's time to see everything work together.

You'll start the server, shorten a real URL, and confirm the full loop: submit a long URL, follow the short link, and verify that click tracking records the visit.

In this step, get ready to:
  • Start the server and shorten a URL using the browser form.
  • Verify the redirect works and check click analytics.
Start the server and shorten a URL

When you start the server, Node.js loads your code and begins listening for incoming requests. It also creates the urls.db SQLite database file on disk if it doesn't already exist.

  • Switch back to your terminal in VS Code.
  • Make sure you're inside the url-shortener folder, then start the server by running this command:
node server.js
  • You should see URL Shortener running at http://localhost:3000 printed in the terminal.

Seeing an error instead?

  • If you see Cannot find module, make sure you ran npm install in the previous step. Your node_modules folder must exist.
  • If you see EADDRINUSE, port 3000 is already in use by another process. Close any other servers running on that port and try again.
  • If you see a syntax error, double check that your server.js and database.js files are saved.

Help me debug my server startup error.

Your server is now running. Leave the terminal open and don't close it.

  • Open your browser and navigate to http://localhost:3000.
  • You should see the URL Shortener page with a text input and a Shorten button.
  • Paste a long URL into the input field. For example, use https://www.google.com/search?q=how+do+url+shorteners+work.
  • Click Shorten.

A green box appears below the form showing your short URL. It looks something like http://localhost:3000/Ab3xK9pQ (your code will be different since it's randomly generated).

What just happened?

When you clicked Shorten, the browser sent a POST request to /api/shorten with your URL. The server validated it, generated an 8-character short code with nanoid, saved the mapping to the SQLite database, and returned the short URL.

You can also confirm the urls.db file was created by checking your project folder in the VS Code Explorer sidebar.

Not seeing the green result box?

  • Make sure you entered a full URL including https://. The server rejects URLs without a valid protocol.
  • Check that the terminal still shows the server running. If you accidentally closed it, run node server.js again.
  • Open DevTools with Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows) and check the Console tab for error messages.

Help me figure out why my form isn't showing results.

Verify the redirect and check click tracking

The whole point of a URL shortener is that visiting the short link takes you to the original destination. The redirect is the core mechanic you need to confirm.

  • Click the short URL link displayed in the green result box.
  • Your browser opens a new tab and redirects you to the original URL you entered.

What is a 302 redirect?

When your browser requests the short URL, the server responds with HTTP status code 302 (temporary redirect) and includes the original URL in the response headers. Your browser automatically follows that redirect and loads the original page.

This is the same mechanism that powers services like Bitly and TinyURL. The server is a middleman that maps short codes to long URLs and tells the browser where to go.

Now confirm that click tracking is working.

  • Copy just the short code from your short URL. This is the 8-character string after http://localhost:3000/ (for example, Ab3xK9pQ).
  • In your browser's address bar, navigate to http://localhost:3000/api/stats/ followed by your short code.

You should see a JSON response that includes your original URL and a clicks count of 1 (or higher if you clicked the short link multiple times).

What does the stats response look like?

The JSON includes every column from the urls table: id, short_code, original_url, created_at, and clicks. Each time someone visits the short link, the incrementClicks function adds 1 to the counter before the redirect happens.

  • Go back to http://localhost:3000 and try shortening a second URL to confirm it works consistently.

Getting a 404 error on the stats page?

  • Double check that you copied the short code correctly. It's case-sensitive.
  • Make sure the full path includes /api/stats/ before the code. Visiting just /stats/ won't work.

Help me figure out why my stats endpoint returns 404.

Secret mission

Add Custom Aliases

Your URL shortener generates random codes. But what if users want memorable short links like localhost:3000/my-portfolio? In this mission, you'll add custom alias support with full validation.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally, so there are no ongoing costs. The urls.db database is just a file on your disk and uses no external services.

Resources you used:

  • The url-shortener project folder on your Desktop (includes source code, node_modules, and the urls.db database file).

Keep everything running

No action needed. Choose this if you're still actively building or want to experiment with your URL shortener further.

  • Your project folder stays on your Desktop as-is.
  • You can restart the server anytime by opening a terminal, navigating to your url-shortener folder, and running node server.js.
  • Your saved URLs in urls.db persist across restarts. You won't lose any data.

Pause - I'll come back to this later

Stop the running server to free up your terminal. Your files and database stay on disk for when you return.

  • If your server is still running, press Ctrl+C in the terminal to stop it.
  • That's it. Your project folder and database stay on disk and cost nothing.
  • When you're ready to come back, open a terminal, navigate to ~/Desktop/url-shortener, and run node server.js to pick up where you left off.

Delete - I don't want to use this again

Remove all project files including the database. This is permanent and cannot be undone.

  • If your server is still running, press Ctrl+C in the terminal to stop it.
  • Delete the entire project folder by running this command for your operating system.

macOS

rm -rf ~/Desktop/url-shortener

Windows

rmdir /s /q %USERPROFILE%\Desktop\url-shortener

This removes all source files, the node_modules folder, and the urls.db database in one step.

Nice Work!

Nice Work!

You just built a fully functional URL shortener from scratch. Your service accepts long URLs, generates unique short codes, stores everything in a persistent database, and redirects visitors back to the original destination.

Here's what you learned:

  • Built a REST API with Express that handles POST requests to shorten URLs and GET requests to redirect visitors using HTTP 302 redirects.
  • Used SQLite with better-sqlite3 to persist URL mappings and click tracking analytics across server restarts.
  • Connected a browser-based frontend form to your API using fetch(), completing the full-stack flow from user input to database to redirect.
  • Added custom alias support so users can choose their own memorable short codes instead of random ones.

Ready to quiz yourself?