← All projects

Node.js · Vercel Functions · JavaScript

URL Shortener

Turns a long URL into a short code and 302-redirects visitors — generation, redirect, and click stats all live in one backend.

Paste a long URL and the backend generates a random short code, stores the mapping, and serves a 302 redirect when it's visited. All three operations (shorten, redirect, stats) are handled by a single serverless function so they share the same in-memory store — a good example of a real constraint you hit once code moves from 'runs on my machine' to serverless.

Live demo

embed
// Every /api/* request is routed through this one serverless function so the
// in-memory link store is shared across shorten/redirect/stats — splitting
// these into separate files would put each on its own Lambda with its own
// memory, and a shortened link would 404 the moment you tried to visit it.
const store = globalThis.__urlStore || (globalThis.__urlStore = new Map());

function randomCode(len = 6) {
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  let out = '';
  for (let i = 0; i < len; i++) out += chars[Math.floor(Math.random() * chars.length)];
  return out;
}

function shorten(req, res) {
  const url = (req.body && req.body.url || '').toString().trim();
  let parsed;
  try {
    parsed = new URL(url);
    if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('bad protocol');
  } catch {
    res.status(400).json({ error: 'a valid http(s) URL is required' });
    return;
  }

  let code = randomCode();
  while (store.has(code)) code = randomCode();
  store.set(code, { url: parsed.toString(), clicks: 0, createdAt: new Date().toISOString() });

  const proto = req.headers['x-forwarded-proto'] || 'https';
  const host = req.headers['x-forwarded-host'] || req.headers.host;
  res.status(201).json({ code, shortUrl: `${proto}://${host}/api/go?code=${code}` });
}

function goTo(req, res, code) {
  const entry = store.get(code);
  if (!entry) {
    res.status(404).send('Short link not found (it may have expired — demo storage is in-memory).');
    return;
  }
  entry.clicks += 1;
  res.writeHead(302, { Location: entry.url });
  res.end();
}

function stats(req, res) {
  const proto = req.headers['x-forwarded-proto'] || 'https';
  const host = req.headers['x-forwarded-host'] || req.headers.host;
  const links = Array.from(store.entries())
    .map(([code, v]) => ({ code, shortUrl: `${proto}://${host}/api/go?code=${code}`, ...v }))
    .sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
  res.status(200).json(links);
}

module.exports = (req, res) => {
  const action = req.query['...action'] || [];
  const [segment] = Array.isArray(action) ? action : [action];

  if (segment === 'shorten' && req.method === 'POST') return shorten(req, res);
  if (segment === 'go' && req.query.code) return goTo(req, res, req.query.code.toString());
  if (segment === 'stats') return stats(req, res);

  res.status(404).json({ error: 'not found' });
};
const form = document.getElementById('form');
const urlInput = document.getElementById('url');
const result = document.getElementById('result');
const shortLink = document.getElementById('short-link');
const errorBox = document.getElementById('error');
const list = document.getElementById('list');

async function loadStats() {
  const res = await fetch('/api/stats');
  const links = await res.json();
  list.innerHTML = '';
  if (!links.length) {
    list.innerHTML = '<p class="empty">No links yet. Shorten one above.</p>';
    return;
  }
  for (const link of links) {
    const row = document.createElement('div');
    row.className = 'item';
    row.innerHTML = `
      <span class="orig"></span>
      <a class="short" target="_blank" rel="noopener"></a>
      <span class="clicks"></span>
    `;
    row.querySelector('.orig').textContent = link.url;
    const a = row.querySelector('.short');
    a.href = link.shortUrl;
    a.textContent = `/${link.code}`;
    row.querySelector('.clicks').textContent = `${link.clicks} click${link.clicks === 1 ? '' : 's'}`;
    list.appendChild(row);
  }
}

form.addEventListener('submit', async (e) => {
  e.preventDefault();
  errorBox.textContent = '';
  result.classList.remove('show');

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

  if (!res.ok) {
    errorBox.textContent = data.error || 'Something went wrong.';
    return;
  }

  shortLink.href = data.shortUrl;
  shortLink.textContent = data.shortUrl;
  result.classList.add('show');
  urlInput.value = '';
  loadStats();
});

loadStats();