← Back to Tutorials
Node.js

How to Build a URL Shortener Web App from Scratch

Difficulty: Intermediate Est. Time: ~3 hours

Introduction

URL shorteners are everywhere in today's internet. Services like bit.ly, TinyURL, and goo.gl have become essential tools for sharing links on social media, in emails, and anywhere character limits matter. But have you ever wondered how these services actually work under the hood?

In this tutorial, you'll build a fully functional URL shortener from scratch using Node.js and Express. By the end, you'll understand the core concepts behind URL shortening and have a working application that you can expand upon.

What You'll Build

A complete URL shortener web application that:

  • Accepts long URLs and generates short codes
  • Stores URL mappings in a database
  • Redirects short URLs to the original long URLs
  • Includes a simple web interface for creating shortened links
  • Tracks click counts for analytics
What You'll Learn
  • Building RESTful APIs with Express.js
  • Working with SQLite for data persistence
  • Generating unique short codes
  • Implementing 301 redirects
  • Basic frontend development

How URL Shorteners Work

Before we start coding, let's understand the fundamental concepts behind URL shortening.

The Core Concept

A URL shortener works on a simple principle: it creates a mapping between a short code and a long URL. When someone visits the short URL, the server looks up the original URL from the database and redirects the user to the destination.

The Flow

  1. User submits a long URL - The user provides a URL like https://example.com/very/long/path/to/some/page
  2. Server generates a short code - The server creates a unique identifier like abc123
  3. Mapping is stored - The mapping abc123 → https://example.com/... is saved in the database
  4. Short URL is returned - The user gets a URL like https://yoursite.com/abc123
  5. Redirect happens - When someone visits the short URL, the server looks up the original and performs a redirect
Why 301 Redirects?

We use HTTP 301 (Moved Permanently) redirects because they tell browsers and search engines that the short URL has permanently moved to the long URL. This also ensures SEO value is passed to the destination URL.

Project Setup

Let's set up our project. We'll use Node.js with Express for the backend and SQLite for simplicity.

Creating the Project

First, create a new folder for your project and initialize it:

Bash
mkdir url-shortener
cd url-shortener
npm init -y

Installing Dependencies

Now let's install the packages we need:

Bash
npm install express sqlite3 shortid

Let's understand what each package does:

  • express - The web framework for building our API and serving the frontend
  • sqlite3 - A lightweight database that stores data in a simple file
  • shortid - A library for generating unique, URL-friendly short codes

Project Structure

Create the following file structure:

File Structure
url-shortener/
├── public/
│   └── index.html
├── server.js
└── database.js

Backend Logic

Let's start building the server. We'll create the main server file that handles all the API endpoints.

JavaScript
const express = require('express');
const shortid = require('shortid');
const db = require('./database');

const app = express();
const PORT = process.env.PORT || 3000;

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

// API: Create a short URL
app.post('/api/shorten', (req, res) => {
    const { longUrl } = req.body;
    
    // Validate URL
    if (!longUrl) {
        return res.status(400).json({ error: 'URL is required' });
    }
    
    // Validate URL format
    try {
        new URL(longUrl);
    } catch (e) {
        return res.status(400).json({ error: 'Invalid URL format' });
    }
    
    // Generate short code
    const shortCode = shortid.generate();
    
    // Save to database
    db.run(
        'INSERT INTO urls (long_url, short_code, clicks, created_at) VALUES (?, ?, 0, datetime("now"))',
        [longUrl, shortCode],
        function(err) {
            if (err) {
                return res.status(500).json({ error: 'Database error' });
            }
            
            res.json({
                shortUrl: `${req.protocol://${req.get('host')}/${shortCode}`,
                shortCode: shortCode,
                longUrl: longUrl
            });
        }
    );
});

// API: Get all URLs
app.get('/api/urls', (req, res) => {
    db.all('SELECT * FROM urls ORDER BY created_at DESC', [], (err, rows) => {
        if (err) {
            return res.status(500).json({ error: 'Database error' });
        }
        res.json(rows);
    });
});

// Redirect: Handle short URLs
app.get('/:shortCode', (req, res) => {
    const { shortCode } = req.params;
    
    db.get(
        'SELECT long_url FROM urls WHERE short_code = ?',
        [shortCode],
        (err, row) => {
            if (err || !row) {
                return res.status(404).sendFile(path.join(__dirname, 'public', '404.html'));
            }
            
            // Increment click count
            db.run('UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?', [shortCode]);
            
            // Redirect with 301 status
            res.redirect(301, row.long_url);
        }
    );
});

// Start server
app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});
Adding the path Module

We need to add the path module at the top of the file. Update your require statements:

JavaScript
const express = require('express');
const path = require('path');
const shortid = require('shortid');
const db = require('./database');

Database Design

We'll use SQLite for our database because it's simple to set up and requires no separate database server. The data will be stored in a single file.

Database Schema

Our database needs a simple table to store URL mappings. Here's what we'll store:

  • id - Auto-incrementing primary key
  • long_url - The original URL that the user wants to shorten
  • short_code - The unique short code (e.g., "abc123")
  • clicks - Count of how many times the short URL has been visited
  • created_at - Timestamp of when the URL was created
JavaScript
const sqlite3 = require('sqlite3').verbose();
const path = require('path');

// Create database file in project directory
const dbPath = path.resolve(__dirname, 'urls.db');
const db = new sqlite3.Database(dbPath);

// Initialize database with schema
db.serialize(() => {
    db.run(`
        CREATE TABLE IF NOT EXISTS urls (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            long_url TEXT NOT NULL,
            short_code TEXT NOT NULL UNIQUE,
            clicks INTEGER DEFAULT 0,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )
    `);
    
    // Create index for faster lookups
    db.run(`
        CREATE INDEX IF NOT EXISTS idx_short_code ON urls(short_code)
    `);
    
    console.log('Database initialized successfully');
});

module.exports = db;
Why Indexes Matter

We created an index on the short_code column. This makes lookups extremely fast even with millions of URLs in the database. Without an index, the database would have to scan every row to find a match!

Redirect Logic

The redirect functionality is what makes a URL shortener work. When a user visits a short URL, the server needs to find the original URL and send the user there.

The Redirect Endpoint

In our server.js, we have this route that handles all short URLs:

JavaScript
// Handle short URL redirects
app.get('/:shortCode', (req, res) => {
    const { shortCode } = req.params;
    
    // Look up the original URL
    db.get(
        'SELECT long_url FROM urls WHERE short_code = ?',
        [shortCode],
        (err, row) => {
            // Handle errors or missing URLs
            if (err || !row) {
                console.log(`Short code not found: ${shortCode}`);
                return res.status(404.send('URL not found'));
            }
            
            // Track clicks for analytics
            db.run(
                'UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?',
                [shortCode]
            });
            
            // Perform 301 redirect (permanent redirect)
            console.log(`Redirecting ${shortCode} -> ${row.long_url}`);
            res.redirect(301, row.long_url);
        }
    );
});

HTTP Redirect Codes

We use HTTP 301 (Moved Permanently) for several reasons:

  • 301 - Permanent redirect. Browsers cache this, making subsequent visits faster. Search engines transfer "link juice" to the destination.
  • 302 - Temporary redirect. Would work but doesn't pass SEO benefits.
  • 307 - Temporary redirect that preserves the HTTP method.
Click Tracking

We increment the click counter before redirecting. This gives us basic analytics - you can see how many times each shortened URL has been visited. Later, you could expand this to track unique visitors, referrers, and more!

Creating a Simple Frontend

Now let's build a simple but clean web interface for our URL shortener. We'll create an HTML page that allows users to enter URLs and see their shortened versions.

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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 2rem;
        }
        
        .container {
            max-width: 800px;
            margin: 0 auto;
        }
        
        h1 {
            color: white;
            text-align: center;
            margin-bottom: 2rem;
        }
        
        .card {
            background: white;
            border-radius: 12px;
            padding: 2rem;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
        }
        
        .input-group {
            display: flex;
            gap: 0.5rem;
            margin-bottom: 1.5rem;
        }
        
        input {
            flex: 1;
            padding: 1rem;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 1rem;
        }
        
        input:focus {
            outline: none;
            border-color: #667eea;
        }
        
        button {
            padding: 1rem 2rem;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: background 0.2s;
        }
        
        button:hover {
            background: #5568d3;
        }
        
        .result {
            background: #f0f4ff;
            padding: 1rem;
            border-radius: 8px;
            margin-bottom: 1.5rem;
            display: none;
        }
        
        .result.show {
            display: block;
        }
        
        .result p {
            margin-bottom: 0.5rem;
            color: #666;
        }
        
        .result a {
            color: #667eea;
            font-weight: 600;
        }
        
        table {
            width: 100%;
            border-collapse: collapse;
        }
        
        th, td {
            text-align: left;
            padding: 0.75rem;
            border-bottom: 1px solid #e0e0e0;
        }
        
        th {
            color: #666;
            font-weight: 600;
        }
        
        .short-link {
            color: #667eea;
            text-decoration: none;
        }
        
        .error {
            color: #e74c3c;
            margin-bottom: 1rem;
            display: none;
        }
        
        .error.show {
            display: block;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🔗 URL Shortener</h1>
        
        <div class="card">
            <div class="error" id="error"></div>
            
            <div class="result" id="result">
                <p>Your shortened URL:</p>
                <a href="" id="short-url" target="_blank"></a>
            </div>
            
            <div class="input-group">
                <input type="url" id="long-url" placeholder="Enter a long URL..." required>
                <button id="shorten-btn">Shorten</button>
            </div>
            
            <table>
                <thead>
                    <tr>
                        <th>Short URL</th>
                        <th>Original URL</th>
                        <th>Clicks</th>
                    </tr>
                </thead>
                <tbody id="url-table">
                </tbody>
            </table>
        </div>
    </div>
    
    <script>
        const longUrlInput = document.getElementById('long-url');
        const shortenBtn = document.getElementById('shorten-btn');
        const resultDiv = document.getElementById('result');
        const shortUrlLink = document.getElementById('short-url');
        const urlTable = document.getElementById('url-table');
        const errorDiv = document.getElementById('error');
        
        // Load existing URLs
        async function loadUrls() {
            const res = await fetch('/api/urls');
            const urls = await res.json();
            
            urlTable.innerHTML = urls.map(url => <?>`
                <tr>
                    <td><a href="/${url.short_code}" class="short-link" target="_blank">/${url.short_code}</a></td>
                    <td title="${url.long_url}">${url.long_url.substring(0, 40)}...</td>
                    <td>${url.clicks}</td>
                </tr>
            `).join('');
        }
        
        // Shorten URL
        async function shortenUrl() {
            const longUrl = longUrlInput.value.trim();
            
            if (!longUrl) return;
            
            try {
                const res = await fetch('/api/shorten', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ longUrl })
                });
                
                const data = await res.json();
                
                if (!res.ok) throw new Error(data.error || 'Error creating short URL');
                
                shortUrlLink.href = data.shortUrl;
                shortUrlLink.textContent = data.shortUrl;
                resultDiv.classList.add('show');
                errorDiv.classList.remove('show');
                
                longUrlInput.value = '';
                loadUrls();
            } catch (err) {
                errorDiv.textContent = err.message;
                errorDiv.classList.add('show');
                resultDiv.classList.remove('show');
            }
        }
        
        shortenBtn.addEventListener('click', shortenUrl);
        
        longUrlInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') shortenUrl();
        });
        
        loadUrls();
    </script>
</body>
</html>

Testing the Application

Now let's test our URL shortener to make sure everything works correctly.

Starting the Server

Run the server with Node.js:

Bash
node server.js

You should see:

Database initialized successfully
Server running on http://localhost:3000

Testing with the Browser

  1. Open your browser and navigate to http://localhost:3000
  2. You should see the URL shortener interface
  3. Enter a long URL (e.g., https://www.google.com/search?q=tutorials)
  4. Click the "Shorten" button
  5. A shortened URL should appear (e.g., http://localhost:3000/V1sT7s)
  6. Click the shortened URL to verify it redirects to the original

Testing the API Directly

You can also test the API using curl or a tool like Postman:

Bash
# Create a short URL
curl -X POST http://localhost:3000/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"longUrl": "https://github.com"}'

# Response:
# {"shortUrl":"http://localhost:3000/abc123","shortCode":"abc123","longUrl":"https://github.com"}
Bash
# Get all URLs
curl http://localhost:3000/api/urls

# Test redirect
curl -I http://localhost:3000/abc123
Testing Checklist
  • ✓ Can create a new short URL
  • ✓ Short URL redirects to original
  • ✓ Click counter increments on redirect
  • ✓ Invalid URLs show error messages
  • ✓ All existing URLs are listed

Deployment Overview

When you're ready to deploy your URL shortener to production, here's what you need to know.

Platform Options

  • Render - Free tier available, easy Node.js deployment
  • Railway - Simple deployment with SQLite support
  • Heroku - Classic choice, requires paid tier for SQLite
  • Vercel/Netlify - Great for frontend, requires adapter for Express

Production Considerations

Database

For production, consider upgrading from SQLite to a more robust database:

  • PostgreSQL - Full-featured, free option on Render/Railway
  • MongoDB - Good for document storage
  • Redis - Ultra-fast for URL lookups

Security

JavaScript
// Add URL validation to prevent malicious redirects
function isValidUrl(string) {
    try {
        const url = new URL(string);
        // Only allow http and https
        return url.protocol === 'http:' || url.protocol === 'https:';
    } catch(_)) {
        return false;
    }
}

// Use in your route handler
if (!isValidUrl(longUrl)) {
    return res.status(400).json({ error: 'Invalid URL' });
}

Analytics Expansion

You could expand your application with:

  • User accounts and authentication
  • Custom short codes (e.g., mysite.com/github)
  • Expiration dates for links
  • Geographic analytics
  • QR code generation
Next Steps

Now that you have a working URL shortener, try these challenges:

  • Add user authentication
  • Implement custom short codes
  • Add a QR code generator
  • Create an admin dashboard

Conclusion

Congratulations! You've successfully built a complete URL shortener from scratch. Let's review what you accomplished:

  • Backend - Created an Express.js server with RESTful API endpoints
  • Database - Set up SQLite with proper schema and indexes
  • URL Shortening - Implemented short code generation using shortid
  • Redirect Logic - Built 301 redirects with click tracking
  • Frontend - Created a clean, functional user interface

This project gives you a solid foundation for understanding how web applications work. The concepts you've learned here apply to many other types of applications - from e-commerce sites to social networks.

Continue Learning

Check out these other tutorials:

  • Build a Weather Dashboard
  • Build a Real-time Chat App
  • Build an E-commerce Store