Only show guides for your app
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.
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.
| 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 |
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>
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.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.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.
public folder for index.html), or push them from Git — see Deploy your code with Git.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.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.
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.GET /api/stats/:code that returns the row, and show the count on a page.expires_at column and skip (or delete) links past their date — the pastebin guide shows that exact pattern.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.
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.links.db (after a reinstall) — the old codes are gone. Shorten the link again.