Building Shortly: A Modern URL Shortener with Node.js, Express, and MongoDB

In today's digital landscape, URL shorteners have become an essential tool for sharing links efficiently across platforms with character limitations. Whether you're sharing links on social media, in email campaigns, or simply wanting cleaner, more manageable URLs, a URL shortener provides a valuable service.

This tutorial will guide you through building your own URL shortener service from scratch using Node.js, Express, and MongoDB. By the end, you'll have a fully functional application that you can host on your own server, customize to your needs, and even extend with additional features.

Table of Contents

  1. Introduction and Project Overview

  2. Setting Up Your Development Environment

  3. Project Architecture and Structure

  4. Backend Development with Express

  5. Database Design with MongoDB

  6. The URL Shortening Algorithm

  7. API Endpoint Implementation

  8. Building the Frontend Interface

  9. Implementing Basic Analytics

  10. Testing Your Application

  11. Deployment Options

  12. Security Considerations

  13. Future Enhancements

  14. Conclusion

<a id="introduction"></a>

1. Introduction and Project Overview

What We're Building

Shortly is a URL shortening service that takes long URLs and creates shortened, easy-to-share links. When users access these shortened links, they are redirected to the original destination. Our application will include:

  • A simple, clean user interface

  • Backend API with Express.js

  • MongoDB database for storing URLs

  • Custom shortening algorithm

  • Basic analytics to track link usage

Why Build a URL Shortener?

URL shorteners are an excellent learning project because they:

  • Demonstrate practical backend and frontend integration

  • Require thoughtful algorithm design

  • Provide opportunities to work with databases

  • Showcase API development

  • Offer a complete user flow from input to output

According to TechCrunch and The Verge, URL shorteners remain one of the most commonly used web utilities, with billions of shortened links generated annually.

Prerequisites

This tutorial assumes you have:

  • Basic knowledge of JavaScript

  • Familiarity with Node.js concepts

  • Understanding of HTTP and RESTful APIs

  • Basic command line skills

  • Node.js and npm installed on your system

<a id="setup"></a>

2. Setting Up Your Development Environment

Installing Required Software

First, ensure you have Node.js installed. You can download it from nodejs.org.

Next, we'll need MongoDB. You have two options:

  • Install MongoDB locally

  • Use MongoDB Atlas (cloud-based solution)

For simplicity, we'll use MongoDB Atlas for this tutorial, as it doesn't require local installation and configuration.

Project Initialization

Create a new directory for your project:

bashmkdir shortlycd shortly

Initialize a new Node.js project:

bashnpm init -y

Installing Dependencies

We'll need several packages for our project:

bashnpm install express mongoose shortid valid-url cors dotenvnpm install --save-dev nodemon

Here's what each package does:

  • express

    : Our web server framework

  • mongoose

    : MongoDB object modeling tool

  • shortid

    : Generates short, unique, non-sequential ids

  • valid-url

    : URL validation

  • cors

    : Enables cross-origin resource sharing

  • dotenv

    : Loads environment variables from a .env file

  • nodemon

    : Development tool that automatically restarts the server

Project Structure

Let's set up a clean project structure:

shortly/│ ├── config/ │ └── db.js # Database connection configuration │ ├── models/ │ └── Url.js # Mongoose model for URLs │ ├── routes/ │ └── urls.js # Express routes for URL operations │ ├── public/ │ ├── css/ │ │ └── style.css # Stylesheet │ └── js/ │ └── app.js # Frontend JavaScript │ ├── views/ │ └── index.html # Main HTML page │ ├── .env # Environment variables ├── server.js # Main application file └── package.json # Project metadata and dependencies

Create these directories and files:

bashmkdir config models routes public public/css public/js viewstouch config/db.js models/Url.js routes/urls.js public/css/style.css public/js/app.js views/index.html server.js .env

<a id="architecture"></a>

3. Project Architecture and Structure

Architectural Overview

Our URL shortener follows a classic three-tier architecture:

  1. Presentation Layer

    : HTML/CSS/JavaScript frontend

  2. Application Layer

    : Express.js backend with RESTful API

  3. Data Layer

    : MongoDB database

The application flow works like this:

  1. User enters a long URL in the frontend

  2. Frontend sends the URL to our API

  3. Backend validates the URL

  4. If valid, the URL is stored in the database and a short code is generated

  5. The short URL is returned to the frontend

  6. When someone visits the short URL, the server looks up the original URL and redirects

Design Decisions

For our shortening algorithm, we'll use the shortid package to generate a unique, short code. While we could implement a custom Base62 encoding system (using a-z, A-Z, 0-9), shortid provides a reliable, collision-resistant solution out of the box.

According to discussions on Hacker News and Stack Overflow Blog, most production URL shorteners use either random string generation or deterministic encoding of database IDs.

<a id="backend"></a>

4. Backend Development with Express

Setting Up the Express Server

Let's start by creating our main server file. Open server.js and add:

javascriptconst express = require('express');const connectDB = require('./config/db');const cors = require('cors');const path = require('path');require('dotenv').config(); const app = express(); // Connect to databaseconnectDB(); // Middlewareapp.use(express.json({ extended: false }));app.use(cors()); // Serve static filesapp.use(express.static(path.join(__dirname, 'public'))); // Define routesapp.use('/api/url', require('./routes/urls')); // Serve main pageapp.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'index.html'));}); // Redirect route for shortened URLsapp.get('/:code', async (req, res) => { try { const url = await require('./models/Url').findOne({ urlCode: req.params.code }); if (url) { // Increment clicks url.clicks++; await url.save(); return res.redirect(url.longUrl); } else { return res.status(404).json('No URL found'); } } catch (err) { console.error(err); res.status(500).json('Server error'); }}); const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Database Configuration

Now let's set up our database connection. Open config/db.js:

javascriptconst mongoose = require('mongoose');require('dotenv').config(); const connectDB = async () => { try { await mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, }); console.log('MongoDB Connected...'); } catch (err) { console.error(err.message); process.exit(1); }}; module.exports = connectDB;

Create a .env file in your project root:

MONGO_URI=your_mongodb_connection_stringBASE_URL=http://localhost:5000 PORT=5000

You'll need to replace your_mongodb_connection_string with your actual MongoDB connection string from MongoDB Atlas or your local MongoDB installation.

<a id="database"></a>

5. Database Design with MongoDB

Creating the URL Schema

For our URL shortener, we need to store several pieces of information:

  • The original (long) URL

  • The generated short code

  • The full short URL

  • Creation date

  • Click count for analytics

Let's create our Mongoose model. Open models/Url.js:

javascriptconst mongoose = require('mongoose'); const urlSchema = new mongoose.Schema({ urlCode: String, longUrl: String, shortUrl: String, date: { type: String, default: Date.now }, clicks: { type: Number, default: 0 }}); module.exports = mongoose.model('Url', urlSchema);

This schema provides a structure for our URL documents in MongoDB. The clicks field will be used for our basic analytics feature.

According to best practices discussed on Dev.to and CSS-Tricks, tracking click counts is an essential feature for any URL shortener service.

<a id="algorithm"></a>

6. The URL Shortening Algorithm

Generating Short Codes

For our URL shortener, we need to generate short, unique codes that we can use to identify the original URLs. We'll use the shortid package for this purpose:

javascriptconst shortid = require('shortid'); // Generate a short codeconst code = shortid.generate();

The shortid package generates short, non-sequential, URL-friendly unique IDs. By default, it uses a 7-10 character string composed of letters and numbers.

If you wanted to implement your own algorithm instead, you could use a Base62 encoding (a-z, A-Z, 0-9) of incremental IDs from your database. This would look something like:

javascriptfunction toBase62(num) { const characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; const base = characters.length; let encoded = ''; if (num === 0) { return characters[0]; } while (num > 0) { encoded = characters[num % base] + encoded; num = Math.floor(num / base); } return encoded;}

However, using shortid is simpler and provides good guarantees against collisions.

<a id="api"></a>

7. API Endpoint Implementation

URL Routes

Let's implement our API endpoints for creating and retrieving shortened URLs. Open routes/urls.js:

javascriptconst express = require('express');const router = express.Router();const validUrl = require('valid-url');const shortid = require('shortid');require('dotenv').config(); const Url = require('../models/Url'); // @route POST /api/url/shorten// @desc Create short URLrouter.post('/shorten', async (req, res) => { const { longUrl } = req.body; const baseUrl = process.env.BASE_URL; // Check base url if (!validUrl.isUri(baseUrl)) { return res.status(401).json('Invalid base URL'); } // Create URL code const urlCode = shortid.generate(); // Check long url if (validUrl.isUri(longUrl)) { try { // Check if the URL already exists in the database let url = await Url.findOne({ longUrl }); if (url) { return res.json(url); } else { // Create the short URL const shortUrl = `${baseUrl}/${urlCode}`; // Create new URL object url = new Url({ longUrl, shortUrl, urlCode, date: new Date() }); // Save to database await url.save(); return res.json(url); } } catch (err) { console.error(err); return res.status(500).json('Server error'); } } else { return res.status(401).json('Invalid long URL'); }}); // @route GET /api/url/:code// @desc Redirect to long URLrouter.get('/:code', async (req, res) => { try { const url = await Url.findOne({ urlCode: req.params.code }); if (url) { return res.json(url); } else { return res.status(404).json('No URL found'); } } catch (err) { console.error(err); res.status(500).json('Server error'); }}); module.exports = router;

This sets up two main endpoints:

  1. POST /api/url/shorten

    - Creates a shortened URL

  2. GET /api/url/:code

    - Retrieves information about a shortened URL

<a id="frontend"></a>

8. Building the Frontend Interface

HTML Structure

Let's create a simple, clean interface for our URL shortener. Open views/index.html:

<html><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Shortly - URL Shortener</title> <link rel="stylesheet" href="/css/style.css"></head><body> <div class="container"> <header> <h1>Shortly</h1> <p>A simple URL shortener built with Node.js, Express, and MongoDB</p> </header> <main> <form id="url-form"> <div class="input-group"> <input type="url" id="long-url" placeholder="Paste your long URL here" required > <button type="submit">Shorten</button> </div> </form> <div id="result" class="hidden"> <h2>Your Shortened URL</h2> <div class="short-url-container"> <input type="text" id="short-url" readonly> <button id="copy-btn">Copy</button> </div> <div class="original-url"> <p>Original URL: <span id="original-url"></span></p> </div> </div> <div id="error" class="hidden"> <p>There was an error shortening your URL. Please try again.</p> </div> </main> <footer> <p>&copy; 2025 Shortly. View the <a href="https://github.com/yourusername/shortly" target="_blank">source code</a>.</p> </footer> </div> <script src="/js/app.js"></script></body></html>

CSS Styling

Now let's add some styling. Open public/css/style.css:

css:root { --primary-color: #3498db; --secondary-color: #2980b9; --dark-color: #2c3e50; --light-color: #ecf0f1; --success-color: #2ecc71; --error-color: #e74c3c;} * { box-sizing: border-box; margin: 0; padding: 0;} body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; background: #f4f7f9;} .container { max-width: 800px; margin: 0 auto; padding: 2rem;} header { text-align: center; margin-bottom: 2rem;} h1 { color: var(--primary-color); margin-bottom: 0.5rem; font-size: 2.5rem;} header p { color: var(--dark-color);} .input-group { display: flex; margin-bottom: 1.5rem;} input[type="url"] { flex: 1; padding: 0.8rem; border: 1px solid #ddd; border-radius: 4px 0 0 4px; font-size: 1rem;} button { background: var(--primary-color); color: white; border: none; padding: 0 1.5rem; border-radius: 0 4px 4px 0; cursor: pointer; font-size: 1rem; transition: background-color 0.3s;} button:hover { background: var(--secondary-color);} #result { background: white; padding: 2rem; border-radius: 4px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); margin-bottom: 2rem;} #result h2 { color: var(--dark-color); margin-bottom: 1rem; font-size: 1.5rem;} .short-url-container { display: flex; margin-bottom: 1rem;} #short-url { flex: 1; padding: 0.8rem; border: 1px solid #ddd; border-radius: 4px 0 0 4px; font-size: 1rem; background: var(--light-color);} #copy-btn { border-radius: 0 4px 4px 0;} .original-url { font-size: 0.9rem; color: #777;} #original-url { word-break: break-all;} #error { background: var(--error-color); color: white; padding: 1rem; border-radius: 4px; margin-bottom: 2rem;} .hidden { display: none;} footer { text-align: center; margin-top: 2rem; color: #777; font-size: 0.9rem;} footer a { color: var(--primary-color); text-decoration: none;} footer a:hover { text-decoration: underline;} @media (max-width: 600px) { .container { padding: 1rem; } h1 { font-size: 2rem; }}

JavaScript Frontend Logic

Now we'll implement the frontend logic to interact with our API. Open public/js/app.js:

javascriptdocument.addEventListener('DOMContentLoaded', () => { const urlForm = document.getElementById('url-form'); const longUrlInput = document.getElementById('long-url'); const resultContainer = document.getElementById('result'); const shortUrlDisplay = document.getElementById('short-url'); const originalUrlDisplay = document.getElementById('original-url'); const copyBtn = document.getElementById('copy-btn'); const errorContainer = document.getElementById('error'); // Form submission urlForm.addEventListener('submit', async (e) => { e.preventDefault(); const longUrl = longUrlInput.value; if (!longUrl) { showError('Please enter a URL to shorten'); return; } try { const response = await fetch('/api/url/shorten', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ longUrl }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData || 'Something went wrong'); } const data = await response.json(); // Display the result showResult(data.shortUrl, data.longUrl); // Reset the form longUrlInput.value = ''; } catch (err) { showError(err.message); } }); // Copy button functionality copyBtn.addEventListener('click', () => { shortUrlDisplay.select(); document.execCommand('copy'); // Visual feedback const originalText = copyBtn.textContent; copyBtn.textContent = 'Copied!'; copyBtn.style.backgroundColor = '#2ecc71'; setTimeout(() => { copyBtn.textContent = originalText; copyBtn.style.backgroundColor = ''; }, 2000); }); // Helper functions function showResult(shortUrl, originalUrl) { // Hide error if it's showing errorContainer.classList.add('hidden'); // Set the values shortUrlDisplay.value = shortUrl; originalUrlDisplay.textContent = originalUrl; // Show the result container resultContainer.classList.remove('hidden'); } function showError(message) { errorContainer.textContent = message; errorContainer.classList.remove('hidden'); resultContainer.classList.add('hidden'); }});

<a id="analytics"></a>

9. Implementing Basic Analytics

Tracking Click Counts

We've already set up a basic analytics feature by adding a clicks field to our URL model. Now, let's create a simple endpoint to retrieve these analytics.

Add this to your routes/urls.js file:

javascript// @route GET /api/url/stats/:code// @desc Get URL statsrouter.get('/stats/:code', async (req, res) => { try { const url = await Url.findOne({ urlCode: req.params.code }); if (url) { return res.json({ urlCode: url.urlCode, longUrl: url.longUrl, shortUrl: url.shortUrl, clicks: url.clicks, date: url.date }); } else { return res.status(404).json('No URL found'); } } catch (err) { console.error(err); res.status(500).json('Server error'); }});

Adding Analytics to the Frontend

Let's add a simple analytics button to our frontend. Add this to your views/index.html file, inside the #result div:

html<div class="analytics"> <button id="stats-btn">View Stats</button> <div id="stats-display" class="hidden"> <p>Clicks: <span id="click-count">0</span></p> <p>Created: <span id="created-date"></span></p> </div></div>

Add this CSS to your style.css:

css.analytics { margin-top: 1rem; border-top: 1px solid #eee; padding-top: 1rem;} #stats-btn { background: var(--dark-color); border-radius: 4px; padding: 0.5rem 1rem; font-size: 0.9rem;} #stats-btn:hover { background: #34495e;} #stats-display { margin-top: 1rem; font-size: 0.9rem; color: #777;}

Add this JavaScript to your app.js:

javascript// Stats button functionalityconst statsBtn = document.getElementById('stats-btn');const statsDisplay = document.getElementById('stats-display');const clickCount = document.getElementById('click-count');const createdDate = document.getElementById('created-date'); statsBtn.addEventListener('click', async () => { try { // Get the URL code from the shortened URL const shortUrl = shortUrlDisplay.value; const urlCode = shortUrl.split('/').pop(); const response = await fetch(`/api/url/stats/${urlCode}`); if (!response.ok) { throw new Error('Could not fetch stats'); } const data = await response.json(); // Update the stats display clickCount.textContent = data.clicks; createdDate.textContent = new Date(data.date).toLocaleString(); // Show the stats display statsDisplay.classList.remove('hidden'); } catch (err) { showError(err.message); }});

According to VentureBeat, providing even basic analytics can significantly increase the value of a URL shortener service to its users.

<a id="testing"></a>

10. Testing Your Application

Manual Testing

Now that we've built our application, let's test it:

  1. Start your server:

bashnpm run dev

  1. Open your browser and navigate to

    http://localhost:5000

  2. Enter a long URL in the input field and click "Shorten"

  3. Copy the short URL and open it in a new tab to verify redirection

  4. Click "View Stats" to see the click count and creation date

Automated Testing

For a more robust application, you might want to add automated tests. Here's how you could set up some basic tests using Jest and Supertest.

First, install the testing dependencies:

bashnpm install --save-dev jest supertest

Then, create a tests directory and add a test file:

bashmkdir teststouch tests/url.test.js

Add this to url.test.js:

javascriptconst request = require('supertest');const mongoose = require('mongoose');const { MongoMemoryServer } = require('mongodb-memory-server');const app = require('../server'); // Make sure to export your Express app from server.js let mongoServer; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); await mongoose.connect(mongoUri);}); afterAll(async () => { await mongoose.disconnect(); await mongoServer.stop();}); describe('URL Shortener API', () => { test('POST /api/url/shorten with valid URL', async () => { const response = await request(app) .post('/api/url/shorten') .send({ longUrl: 'https://www.example.com' }); expect(response.status).toBe(200); expect(response.body).toHaveProperty('shortUrl'); expect(response.body).toHaveProperty('longUrl', 'https://www.example.com'); }); test('POST /api/url/shorten with invalid URL', async () => { const response = await request(app) .post('/api/url/shorten') .send({ longUrl: 'invalidurl' }); expect(response.status).toBe(401); });});

Add a test script to your package.json:

json"scripts": { "test": "jest"}

Run the tests:

bashnpm test

<a id="deployment"></a>

11. Deployment Options

Hosting Options

When you're ready to deploy your URL shortener, you have several options:

  1. Heroku

    • Easy to set up and deploy

    • Free tier available for hobby projects

    • Supports Node.js applications

  2. Vercel or Netlify

    • Great for frontend hosting

    • Can be combined with serverless functions

  3. DigitalOcean or AWS

    • More control over your environment

    • More scalable but more complex to set up

  4. Railway or Render

    • Modern platforms with simple deploys

    • Good balance of control and ease of use

Deploying to Heroku

Let's walk through deploying to Heroku:

  1. Install the Heroku CLI

  2. Login to Heroku:

bashheroku login

  1. Create a

    Procfile

    in your project root:

web: node server.js

  1. Initialize a Git repository (if you haven't already):

bashgit initgit add .git commit -m "Initial commit"

  1. Create a Heroku app:

bashheroku create shortly-app-name

  1. Set up environment variables:

bashheroku config:set MONGO_URI=your_production_mongodb_uriheroku config:set BASE_URL=https://your-app-name.herokuapp.com

  1. Push to Heroku:

bashgit push heroku master

<a id="security"></a>

12. Security Considerations

Protecting Your URL Shortener

URL shorteners can be targets for abuse, so it's important to implement security measures:

  1. Rate Limiting

    • Limit the number of URLs that can be created per IP address

    • Use a package like

      express-rate-limit

javascriptconst rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // limit each IP to 100 requests per windowMs}); app.use('/api/url', limiter);

  1. URL Validation

    • We're already using

      valid-url

      to validate URLs

    • Consider additional checks for malicious URLs

  2. HTTPS

    • Ensure your production deployment uses HTTPS

    • Redirect HTTP requests to HTTPS

  3. Preview Page

    • Consider adding a preview page before redirecting

    • This can protect users from malicious redirects

<a id="enhancements"></a>

13. Future Enhancements

Taking Your URL Shortener Further

Once you have the basic URL shortener working, here are some features you could add:

  1. Custom Short Codes

    • Allow users to choose their own short codes

    • Validate to ensure no duplicates

  2. User Accounts

    • Add authentication

    • Allow users to manage their shortened URLs

  3. Advanced Analytics

    • Track referrers, geographical data, browser info

    • Provide visualizations of this data

  4. QR Code Generation

    • Generate QR codes for shortened URLs

    • Useful for print materials and physical displays

  5. Link Expiration

    • Set expiration dates for links

    • Automatically remove expired links

  6. Password Protection

    • Allow links to be protected with passwords

    • Require a password before redirecting

  7. API Keys

    • Provide an API for programmatic URL shortening

    • Issue API keys to control access

<a id="conclusion"></a>

14. Conclusion

Congratulations! You've built a complete URL shortener application using Node.js, Express, and MongoDB. This project demonstrates many important web development concepts:

  • RESTful API design

  • Database modeling

  • Frontend/backend integration

  • Form handling and validation

  • Error handling

  • Basic analytics

  • Deployment considerations

The skills you've learned here can be applied to many other web development projects. As mentioned in the Future Enhancements section, there are many ways you could extend this project to add more features and functionality.

Remember that URL shorteners, while seemingly simple, are a critical part of the modern web infrastructure. According to MIT Technology Review, billions of shortened URLs are created and clicked every day, serving a crucial role in social media, marketing, and general web usability.

I hope you've enjoyed building this project and that it serves as a valuable addition to your portfolio!

References and Further Reading