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 theCREATE TABLE IF NOT EXISTSruns every start so a fresh server sets itself up.better-sqlite3ships 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. Thedo…whileloop re-rolls on the tiny chance the code already exists, so no two links ever collide. - Two routes carry the whole app.
POST /api/shortenvalidates the URL, stores it, and returns the code.GET /:codeis the redirect: it looks the code up, incrementsclicks, and sends a 302 to the original. Becauseexpress.staticis 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
- Put the three files on your server with the File Manager (create the
publicfolder forindex.html), or push them from Git — see Deploy your code with Git. - Press Start on the Console page. The first boot runs
npm install(you'll see Express andbetter-sqlite3fetched), then your app printsListening on port …— the line that flips the server to online. - 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
aliasin the POST body and use it as the code (after checking it's free) so you can make/docsinstead of a random string. - A stats page. You're already counting clicks. Add
GET /api/stats/:codethat returns the row, and show the count on a page. - Expiry. Add an
expires_atcolumn and skip (or delete) links past their date — the pastebin guide shows that exact pattern. - A real domain. Swap the ugly
address:portfor 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
listencall must useSERVER_PORTand0.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.