Nie wieder Bit.ly: Baue deinen eigenen URL-Shortener
Jeder kennt Bit.ly, TinyURL & Co. – aber warum seine Daten durch fremde Server jagen, wenn man das Ganze in wenigen Minuten selbst bauen kann? In diesem Tutorial erstellst du einen vollständigen URL-Shortener mit eigener Datenbank, Redirect-Logik und einer minimalen, aber schicken Oberfläche.
Was du lernst:
- REST-API-Design mit Express
- Datenbank-Grundlagen mit SQLite (ohne externen Server!)
- Zufällige Short-IDs generieren
- Deployment mit Docker Compose
🛠️ Projekt aufsetzen
mkdir my-url-shortener && cd my-url-shortener
npm init -y
npm install express better-sqlite3
1. Die Datenbank (SQLite)
SQLite ist perfekt für diesen Use-Case: Kein separater Datenbankserver nötig, alles lebt in einer einzigen Datei.
my-url-shortener/db.js:
const Database = require('better-sqlite3');
const db = new Database('urls.db');
// Tabelle erstellen (falls nicht vorhanden)
db.exec(`
CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
short TEXT UNIQUE NOT NULL,
original TEXT NOT NULL,
clicks INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
module.exports = db;
2. Der Bauplan (package.json)
Bevor wir Funktionen programmieren, legen wir fest, welche Werkzeuge wir nutzen. Die package.json ist das Gehirn unserer Node-Infrastruktur – sie definiert nicht nur Abhängigkeiten, sondern auch den entscheidenden Start-Befehl für Docker.
my-url-shortener/package.json:
{
"name": "my-url-shortener",
"version": "1.0.0",
"scripts": { "start": "node server.js" },
"dependencies": {
"better-sqlite3": "^9.0.0",
"express": "^4.18.2"
}
}
3. Short-ID Generator
Wir generieren zufällige, kurze Codes. Keine externe Library nötig.
my-url-shortener/utils.js:
function generateShortId(length = 6) {
const chars =
'abcdefghijklmnopqrstuvwxyz' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(
Math.floor(Math.random() * chars.length)
);
}
return result;
}
module.exports = { generateShortId };
4. Der Server (Express)
Das Herzstück: Eine API zum Erstellen und Auflösen von Short-Links.
my-url-shortener/server.js:
const express = require('express');
const path = require('path');
const db = require('./db');
const { generateShortId } = require('./utils');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
// API: Neuen Short-Link erstellen
app.post('/api/shorten', (req, res) => {
const { url } = req.body;
if (!url) {
return res.status(400)
.json({ error: 'URL fehlt' });
}
const short = generateShortId();
const stmt = db.prepare(
'INSERT INTO urls (short, original) VALUES (?, ?)'
);
stmt.run(short, url);
res.json({
short,
shortUrl: `${req.protocol}://${req.get('host')}/${short}`,
original: url
});
});
// API: Alle Links auflisten
app.get('/api/links', (req, res) => {
const links = db.prepare(
'SELECT * FROM urls ORDER BY created_at DESC'
).all();
res.json(links);
});
// Redirect: Short-Link auflösen
app.get('/:short', (req, res) => {
const row = db.prepare(
'SELECT * FROM urls WHERE short = ?'
).get(req.params.short);
if (!row) return res.status(404).send('Link nicht gefunden');
// Klick-Zähler erhöhen
db.prepare(
'UPDATE urls SET clicks = clicks + 1 WHERE short = ?'
).run(req.params.short);
res.redirect(row.original);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(
`URL-Shortener läuft auf http://localhost:${PORT}`
);
});
5. Das Frontend
Eine minimalistische Oberfläche zum Verkürzen und Anzeigen der Links.
my-url-shortener/public/index.html:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>URL Shortener</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, sans-serif;
background: #0f172a;
color: #e2e8f0;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 3rem 1rem;
}
#app {
width: 100%;
max-width: 600px;
}
h1 {
text-align: center;
margin-bottom: 2rem;
color: #38bdf8;
}
.input-group {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
input {
flex-grow: 1;
padding: 0.75rem 1rem;
border: 1px solid #334155;
border-radius: 0.5rem;
background: #1e293b;
color: #e2e8f0;
font-size: 1rem;
outline: none;
}
input:focus { border-color: #38bdf8; }
button {
padding: 0.75rem 1.5rem;
background: #38bdf8;
color: #0f172a;
border: none;
border-radius: 0.5rem;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
button:hover {
background: #0ea5e9;
transform: translateY(-1px);
}
.result {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 2rem;
display: none;
text-align: center;
}
.result a {
color: #38bdf8;
font-size: 1.2rem;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: left;
padding: 0.75rem;
border-bottom: 1px solid #334155;
font-size: 0.85rem;
}
th { color: #94a3b8; }
td a {
color: #38bdf8;
text-decoration: none;
}
.clicks {
color: #10b981;
font-weight: bold;
}
</style>
</head>
<body>
<div id="app">
<h1>🔗 URL Shortener</h1>
<div class="input-group">
<input type="url" id="urlInput"
placeholder="https://beispiel.de/langer-link">
<button onclick="shorten()">Kürzen</button>
</div>
<div class="result" id="result">
Dein Kurzlink: <a id="shortLink"
href="#"
target="_blank"></a>
</div>
<table>
<thead>
<tr>
<th>Kurzlink</th>
<th>Original</th>
<th>Klicks</th>
</tr>
</thead>
<tbody id="linkList"></tbody>
</table>
</div>
<script>
async function shorten() {
const url = document.getElementById('urlInput').value;
if (!url) return;
const resp = await fetch('/api/shorten', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await resp.json();
const result = document.getElementById('result');
const link = document.getElementById('shortLink');
link.href = data.shortUrl;
link.textContent = data.shortUrl;
result.style.display = 'block';
document.getElementById('urlInput').value = '';
loadLinks();
}
async function loadLinks() {
const resp = await fetch('/api/links');
const links = await resp.json();
const tbody = document.getElementById('linkList');
tbody.innerHTML = links.map(l => `
<tr>
<td>
<a href="/${l.short}"
target="_blank">/${l.short}</a>
</td>
<td>${l.original.substring(0, 35)}...</td>
<td class="clicks">${l.clicks}</td>
</tr>
`).join('');
}
loadLinks();
</script>
</body>
</html>
5. Docker Deployment
my-url-shortener/docker-compose.yml:
version: '3.8'
services:
app:
image: node:18-alpine
working_dir: /app
volumes:
- .:/app
ports:
- "3000:3000"
command: sh -c "npm install && npm start"
docker compose up -d
Öffne http://localhost:3000 – fertig ist dein eigener URL-Shortener!
⚠️ 📸 SCREENSHOT ANFRAGE: Hier einen Screenshot der fertigen Anwendung einfügen, idealerweise mit bereits gekürzten Links und sichtbarem Klick-Zähler.
🧠 Was passiert unter der Haube?
- User gibt URL ein → POST an
/api/shorten - Server generiert eine 6-stellige Short-ID
- SQLite speichert das Mapping
short → original - Beim Aufruf von
/abc123wird der Klick gezählt und auf die Original-URL redirected
Das Schöne: Die SQLite-Datei urls.db liegt direkt im Projektordner. Kein externer Datenbankserver, kein Konfigurationsaufwand.
📈 Erweiterungs-Ideen
| Feature | Schwierigkeit | Beschreibung |
|---|---|---|
| Custom Slugs | ⭐ | Eigene Kurznamen statt zufälliger IDs |
| QR-Code Generation | ⭐⭐ | Automatisch einen QR-Code zum Short-Link generieren |
| Ablaufdatum | ⭐⭐ | Links nach X Tagen automatisch deaktivieren |
| Auth & Dashboard | ⭐⭐⭐ | Login-System mit persönlichem Link-Dashboard |
Fazit
In unter 100 Zeilen Backend-Code hast du einen voll funktionsfähigen URL-Shortener gebaut – mit Datenbank, Klick-Tracking und einem sauberen Frontend. Du bist jetzt unabhängig von Bit.ly und hast volle Kontrolle über deine Links.
Viel Spaß beim Kürzen! 🔗
Login