Build a URL shortener

A complete link-shortening service in three files — Express takes a long URL, stores it in SQLite, and hands back a short code that redirects. Deploy it, test it with curl, and extend it with click counts.

Skip the setup This guide has a one-click starter template that installs everything below onto your server.

By the end of this guide you'll have a working URL shortener online: paste in a long link, get back a short one like yoursite/b7K2p, and every visit to that short code redirects to the original. It's three small files on the Node.js application — Express for the web part, SQLite for storage — and it's a great first "real" project because it touches routing, a database, and redirects all at once.

At a glance
You're building A link shortener with a web form and a redirect route
You need A Falix server running the Node.js application
Plan Any — free runs while your session timer has time, premium runs 24/7
Time About thirty minutes

New to the Node.js application? Read Node.js on Falix first — it covers the index.js entry file and automatic npm install this build relies on.

What it does

Feature How
Shorten a URL POST /api/shorten with a JSON { "url": "..." } returns a short code
Redirect GET /:code looks up the code and 302-redirects to the original URL
Web form public/index.html posts to the API and shows the short link
Persistent storage Links live in a SQLite file (links.db) that survives restarts
Click counting Every redirect bumps a clicks column — ready for a stats page later

The files

Three files: package.json (dependencies), index.js (the whole server), and public/index.html (the form).

package.json — this is what makes npm install fetch Express and SQLite automatically on start:

{
  "name": "url-shortener",
  "version": "1.0.0",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "better-sqlite3": "^11.8.1",
    "express": "^4.21.2"
  }
}

index.js — the server:

const express = require('express');
const Database = require('better-sqlite3');
const crypto = require('node:crypto');
const path = require('node:path');

// The database is a single file next to your app.
const db = new Database('links.db');
db.exec(`CREATE TABLE IF NOT EXISTS links (
  code       TEXT PRIMARY KEY,
  url        TEXT NOT NULL,
  clicks     INTEGER NOT NULL DEFAULT 0,
  created_at INTEGER NOT NULL
)`);

const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

// A short random code like "b7K2p".
function makeCode() {
  return crypto.randomBytes(4).toString('base64url').slice(0, 6);
}

// Create a short link.
app.post('/api/shorten', (req, res) => {
  const { url } = req.body;
  if (!url || !/^https?:\/\//i.test(url)) {
    return res.status(400).json({ error: 'Provide a valid http(s) URL' });
  }
  let code;
  do {
    code = makeCode();
  } while (db.prepare('SELECT 1 FROM links WHERE code = ?').get(code));
  db.prepare('INSERT INTO links (code, url, created_at) VALUES (?, ?, ?)')
    .run(code, url, Date.now());
  res.json({ code, short: `/${code}` });
});

// Follow a short link.
app.get('/:code', (req, res) => {
  const row = db.prepare('SELECT url FROM links WHERE code = ?').get(req.params.code);
  if (!row) return res.status(404).send('No such link');
  db.prepare('UPDATE links SET clicks = clicks + 1 WHERE code = ?').run(req.params.code);
  res.redirect(302, row.url);
});

const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));

public/index.html — a tiny form so you don't need curl to use it:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>URL Shortener</title>
</head>
<body>
  <h1>Shorten a URL</h1>
  <form id="form">
    <input id="url" type="url" placeholder="https://example.com/very/long/link" required>
    <button>Shorten</button>
  </form>
  <div id="out"></div>
  <script>
    document.getElementById('form').addEventListener('submit', async (e) => {
      e.preventDefault();
      const url = document.getElementById('url').value;
      const res = await fetch('/api/shorten', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url }),
      });
      const data = await res.json();
      const out = document.getElementById('out');
      if (data.short) {
        const full = location.origin + data.short;
        out.innerHTML = 'Short link: <a href="' + full + '">' + full + '</a>';
      } else {
        out.textContent = data.error || 'Something went wrong';
      }
    });
  </script>
</body>
</html>

How it works

  • Storage is one file. new Database('links.db') opens (or creates) a SQLite file right next to your code, and the CREATE TABLE IF NOT EXISTS runs every start so a fresh server sets itself up. better-sqlite3 ships prebuilt binaries, so it installs on the Node.js application with no compiler — see SQLite in depth.
  • Codes are random and unique. makeCode() turns four random bytes into a short URL-safe string. The do…while loop re-rolls on the tiny chance the code already exists, so no two links ever collide.
  • Two routes carry the whole app. POST /api/shorten validates the URL, stores it, and returns the code. GET /:code is the redirect: it looks the code up, increments clicks, and sends a 302 to the original. Because express.static is registered first, real files (like the home page) win over the catch-all /:code.
  • ?-placeholders, always. Every value goes into the query through a ? parameter, never string-concatenation — that's what keeps SQL injection out.

🎯 Good to know: The redirect route is /:code, a single path segment, so it never swallows /api/shorten (two segments) or your static files. Add any new API routes under /api/... and they'll stay clear of it.

Run it on Falix

  1. Put the three files on your server with the File Manager (create the public folder for index.html), or push them from Git — see Deploy your code with Git.
  2. Press Start on the Console page. The first boot runs npm install (you'll see Express and better-sqlite3 fetched), then your app prints Listening on port … — the line that flips the server to online.
  3. Open your server's address from the Network page in a browser. Paste a long URL into the form, and you'll get a short link back. Click it — you land on the original.

The two Falix rules are already baked in: the code reads SERVER_PORT and binds 0.0.0.0, so it comes up on your public port with nothing to configure. If either were wrong you'd get the classic "works locally, not here" failure — see Your first web app.

Prefer the command line? Here's the round-trip:

curl -X POST http://YOUR_ADDRESS:PORT/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"url": "https://falix.gg/guides"}'
# -> {"code":"b7K2p","short":"/b7K2p"}

curl -i http://YOUR_ADDRESS:PORT/b7K2p
# -> HTTP/1.1 302 Found  /  Location: https://falix.gg/guides

🎯 Good to know: Your links live in links.db, a file on the server. A restart keeps it; a reinstall or application switch wipes it along with everything else. If the links matter, either back the file up (Backups) or move storage to a managed database, which outlives the server.

Make it yours

  • Custom aliases. Accept an optional alias in the POST body and use it as the code (after checking it's free) so you can make /docs instead of a random string.
  • A stats page. You're already counting clicks. Add GET /api/stats/:code that returns the row, and show the count on a page.
  • Expiry. Add an expires_at column and skip (or delete) links past their date — the pastebin guide shows that exact pattern.
  • A real domain. Swap the ugly address:port for a proper name with automatic HTTPS via the Network page's Reverse Proxy — Domains and HTTPS.

Everything past the Falix layer is standard Express — the official guide at expressjs.com takes routing, middleware, and error handling from here.

Troubleshooting

  • Page won't load / connection refused — a port or bind-address problem, not a code one. The listen call must use SERVER_PORT and 0.0.0.0; see I can't reach my app.
  • Cannot find module 'express' or 'better-sqlite3' — the install didn't run or failed. Scroll to the top of the console to read the npm output, or install from the Packages page and restart.
  • Every short link 404s — the code isn't in the database. That usually means a fresh links.db (after a reinstall) — the old codes are gone. Shorten the link again.
  • Short code shows the "No such link" page instead of redirecting — you typed a code that was never created, or the database file was replaced. Check the code against what the API returned.

Next steps

Was this guide helpful?