Bit.ly, TinyURL gibi servisleri her gun kullaniyoruz. Peki bu servislerin arkasindaki mekanizma nasil calisiyor? Cevap sasirtici derecede basit: bir URL al, kisa bir kimlik uret, ikisini esle, yonlendir.
Bu rehberde adim adim kendi URL kisaltici servisinizi yazacaksiniz. Sifirdan. Hazir kutuphane yok, sihirli framework yok — sadece Node.js, Express ve biraz mantik.
Bu rehberde ne ogreneceksiniz:
- Express ile HTTP sunucu kurmak
- nanoid kutuphanesiyle kisa, benzersiz kimlikler uretmek
- In-memory veri yapisiyla URL'leri depolamak
- POST endpointi ile yeni URL kisaltmak
- GET endpointi ile 301 yonlendirme yapmak
- Istatistik endpointi ile tiklama sayisini takip etmek
- Input validation ve guvenlik onlemleri uygulamak
Adim 1: Proje Kurulumu
Terminal acin ve yeni bir klasor olusturun:
mkdir url-shortener && cd url-shortener
npm init -y
npm install express nanoid@3
nanoid@3 versiyonunu kullaniyoruz cunku v3 CommonJS (require) destekliyor. v4 ve uzeri sadece ESM (import) ile calisir — bu rehberde isleri basit tutmak icin CommonJS tercih ediyoruz.
Kurulum sonrasi package.json dosyaniz soyle gorunmeli:
{
"name": "url-shortener",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js"
},
"dependencies": {
"express": "^4.21.0",
"nanoid": "^3.3.8"
}
}
node --watch Node.js 18+ ile gelen yerlesik dosya izleyici. Dosya degistiginde sunucuyu otomatik yeniden baslatir — nodemon kurmaya gerek yok.
Adim 2: Express Sunucu
index.js dosyasini olusturun:
const express = require('express');
const { nanoid } = require('nanoid');
const app = express();
const PORT = process.env.PORT || 3000;
// JSON body parsing
app.use(express.json());
// In-memory store
const urlStore = new Map();
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', urlCount: urlStore.size });
});
app.listen(PORT, () => {
console.log(`URL Shortener calisiyor: http://localhost:${PORT}`);
});
Test edin:
npm start
# Baska terminal:
curl http://localhost:3000/health
Yanit: {"status":"ok","urlCount":0}
Bu kadar. Calisan bir HTTP sunucunuz var. Simdi islevleri eklemeye baslayalim.
Adim 3: nanoid ile Kisa ID Uretimi
URL kisaltici servisinin kalbi, uzun URL'yi temsil edecek kisa ve benzersiz bir kimlik uretmek. Bunun icin nanoid kullaniyoruz.
const { nanoid } = require('nanoid');
// Varsayilan: 21 karakter
console.log(nanoid()); // "V1StGXR8_Z5jdHi6B-myT"
// Kisa versiyon: 8 karakter
console.log(nanoid(8)); // "xQ3fK9pL"
Neden UUID degil de nanoid?
UUID 36 karakter uretir:
550e8400-e29b-41d4-a716-446655440000. URL kisaltici icin bu absurt derecede uzun. nanoid 8 karakterle ayni isi gorur, URL-safe karakter seti kullanir (A-Za-z0-9_-) ve UUID'den %60 daha kucuk boyutludur. Carpisma olasiligi? 8 karakterlik nanoid ile saniyede 1.000 ID uretseniz, %1 carpisma olasiligi icin yaklasik 5.500 yil beklemeniz gerekir.
Adim 4: In-Memory Store
Veritabani karmasikligi eklemeden once, JavaScript'in Map yapisini kullanacagiz. Map, key-value ikilileri icin Object'ten daha performansli ve temiz bir API sunar.
const urlStore = new Map();
// Veri yapisi: shortId -> { originalUrl, createdAt, clicks }
Her URL kaydinda uc bilgi tutuyoruz:
originalUrl— Kullanicinin girdigi uzun URLcreatedAt— Kayit tarihi (istatistik icin)clicks— Tiklama sayaci (0'dan baslar)
Bu yapi uretim icin yeterli degil (sunucu yeniden basladiginda veriler kaybolur), ama konsepti anlamak icin kusursuz. Rehberin sonunda kalici depolama alternatiflerini ele alacagiz.
Adim 5: POST Endpoint — URL Kisaltma
Ilk gercek endpoint. Kullanici uzun bir URL gonderir, biz kisa bir link donuyoruz:
app.post('/api/shorten', (req, res) => {
const { url } = req.body;
// Validation: URL bos mu?
if (!url) {
return res.status(400).json({ error: 'url alani zorunlu' });
}
// Validation: Gecerli URL mi?
try {
new URL(url);
} catch {
return res.status(400).json({ error: 'Gecerli bir URL girin (https://... ile baslamali)' });
}
// Kisa ID uret
const shortId = nanoid(8);
// Kaydet
urlStore.set(shortId, {
originalUrl: url,
createdAt: new Date().toISOString(),
clicks: 0,
});
// Yanit
const shortUrl = `${req.protocol}://${req.get('host')}/${shortId}`;
res.status(201).json({
shortId,
shortUrl,
originalUrl: url,
});
});
Test edin:
curl -X POST http://localhost:3000/api/shorten \
-H "Content-Type: application/json" \
-d '{"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide"}'
Yanit:
{
"shortId": "xQ3fK9pL",
"shortUrl": "http://localhost:3000/xQ3fK9pL",
"originalUrl": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide"
}
new URL() constructor'u hem format hem protokol kontrolu yapar. htp://yanlis veya sadece-metin gibi girdiler hata firlatir. Ayri bir regex yazmaya gerek yok.
Adim 6: GET Endpoint — Yonlendirme
Kisaltilmis URL'ye gelen istekleri orijinal adrese yonlendiriyoruz:
app.get('/:shortId', (req, res) => {
const { shortId } = req.params;
const entry = urlStore.get(shortId);
if (!entry) {
return res.status(404).json({ error: 'URL bulunamadi' });
}
// Tiklama sayacini artir
entry.clicks += 1;
// 301 kalici yonlendirme
res.redirect(301, entry.originalUrl);
});
Test edin:
# -L: redirect'i takip et, -v: detayli cikti
curl -L -v http://localhost:3000/xQ3fK9pL
301 mi 302 mi?
301 (Moved Permanently): Tarayici ve arama motorlari bu yonlendirmeyi onbellegine alir. Sonraki ziyaretlerde sunucuya sormadan dogrudan hedef URL'ye gider. SEO degerini hedef sayfaya aktarir. Dezavantaj: tiklama sayaci dogru calismayabilir cunku tarayici onbellekten yonlendirir.
302 (Found / Temporary): Her seferinde sunucuya sorar. Tiklama sayaci her zaman dogru calisir. SEO degerini aktarmaz.
URL kisaltici icin 301 tercih edilir cunku hedef URL genellikle degismez ve performans avantaji saglar. Eger tiklama analitigini hassas olcmek istiyorsaniz 302 kullanin.
Adim 7: Istatistik Endpoint
Her kisaltilmis URL'nin performansini izlemek icin bir istatistik endpoint'i ekliyoruz:
app.get('/api/stats/:shortId', (req, res) => {
const { shortId } = req.params;
const entry = urlStore.get(shortId);
if (!entry) {
return res.status(404).json({ error: 'URL bulunamadi' });
}
res.json({
shortId,
originalUrl: entry.originalUrl,
createdAt: entry.createdAt,
clicks: entry.clicks,
});
});
Test edin:
curl http://localhost:3000/api/stats/xQ3fK9pL
Yanit:
{
"shortId": "xQ3fK9pL",
"originalUrl": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide",
"createdAt": "2026-03-17T10:30:00.000Z",
"clicks": 3
}
Onemli: /api/stats/:shortId rotasini /:shortId rotasindan once tanimlayin. Express rotalari siraya gore eslestirir. Eger /:shortId once gelirse, stats kelimesini bir shortId olarak yorumlar ve 404 doner.
API Endpoint Tablosu
| Method | Path | Body | Yanit | Aciklama |
|---|---|---|---|---|
| POST | /api/shorten | { "url": "https://..." } | { shortId, shortUrl, originalUrl } | Yeni URL kisalt |
| GET | /:shortId | — | 301 Redirect | Orijinal URL'ye yonlendir |
| GET | /api/stats/:shortId | — | { shortId, originalUrl, createdAt, clicks } | Tiklama istatistikleri |
| GET | /health | — | { status, urlCount } | Sunucu saglik kontrolu |
Bonus: Input Validation ve Rate Limiting
Uretim ortaminda iki onemli guvenlik katmani eklemeniz gerekir:
URL Format Kontrolu
Zaten new URL() ile temel kontrol yapiyoruz. Bunu biraz genisletelim:
function isValidUrl(input) {
try {
const parsed = new URL(input);
// Sadece http ve https protokollerini kabul et
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
// Kullanim
if (!isValidUrl(url)) {
return res.status(400).json({ error: 'Sadece http/https URL kabul edilir' });
}
Bu kontrol javascript:alert(1) veya ftp://dosya.txt gibi tehlikeli veya istenmeyen protokolleri engeller.
Rate Limiting
Kotu niyetli kullanicilarin API'nizi spamlamasini onlemek icin basit bir rate limiter ekleyin:
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 dakika
max: 100, // pencere basina 100 istek
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Cok fazla istek. 15 dakika sonra tekrar deneyin.' },
});
// Sadece kisaltma endpoint'ine uygula
app.use('/api/shorten', limiter);
Tam Calisir Kod
Tum parcalari birlestiren, kopyala-yapistir ile hemen calisir index.js:
const express = require('express');
const { nanoid } = require('nanoid');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
const urlStore = new Map();
// --- Yardimci ---
function isValidUrl(input) {
try {
const parsed = new URL(input);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
// --- Rotalar ---
// Saglik kontrolu
app.get('/health', (req, res) => {
res.json({ status: 'ok', urlCount: urlStore.size });
});
// URL kisalt
app.post('/api/shorten', (req, res) => {
const { url } = req.body;
if (!url) {
return res.status(400).json({ error: 'url alani zorunlu' });
}
if (!isValidUrl(url)) {
return res.status(400).json({ error: 'Gecerli bir http/https URL girin' });
}
const shortId = nanoid(8);
urlStore.set(shortId, {
originalUrl: url,
createdAt: new Date().toISOString(),
clicks: 0,
});
const shortUrl = `${req.protocol}://${req.get('host')}/${shortId}`;
res.status(201).json({ shortId, shortUrl, originalUrl: url });
});
// Istatistikler
app.get('/api/stats/:shortId', (req, res) => {
const entry = urlStore.get(req.params.shortId);
if (!entry) {
return res.status(404).json({ error: 'URL bulunamadi' });
}
res.json({
shortId: req.params.shortId,
originalUrl: entry.originalUrl,
createdAt: entry.createdAt,
clicks: entry.clicks,
});
});
// Yonlendirme
app.get('/:shortId', (req, res) => {
const entry = urlStore.get(req.params.shortId);
if (!entry) {
return res.status(404).json({ error: 'URL bulunamadi' });
}
entry.clicks += 1;
res.redirect(301, entry.originalUrl);
});
app.listen(PORT, () => {
console.log(`URL Shortener calisiyor: http://localhost:${PORT}`);
});
Test Komutlari
Sunucuyu baslatip su komutlari sirayla calistirin:
# 1. URL kisalt
curl -s -X POST http://localhost:3000/api/shorten \
-H "Content-Type: application/json" \
-d '{"url": "https://nodejs.org/en/docs"}' | jq .
# 2. Yonlendirmeyi test et (shortId'yi yukaridaki yanittan alin)
curl -I http://localhost:3000/BURAYA_SHORT_ID
# 3. Istatistik kontrol et
curl -s http://localhost:3000/api/stats/BURAYA_SHORT_ID | jq .
# 4. Gecersiz URL dene
curl -s -X POST http://localhost:3000/api/shorten \
-H "Content-Type: application/json" \
-d '{"url": "bu-gecersiz"}' | jq .
Sonraki Adimlar
Bu rehberdeki URL kisaltici tamamen islevsel ama uretim ortami icin birkac kritik gelistirme gerekir:
-
Veritabani entegrasyonu — In-memory Map yerine Redis (hiz icin) veya PostgreSQL/SQLite (kalicilik icin). Sunucu yeniden basladiginda veriler kaybolmaz.
-
Ozel slug destegi — Kullanicinin kendi kisa adini secmesi:
/api/shortenbody'sinecustomSlugalani ekleyin, benzersizlik kontrolu yapin. -
QR kod uretimi —
qrcodenpm paketi ile her kisaltilmis URL icin QR kod olusturun. Fiziksel pazarlama materyalleri icin cok degerli. -
Analitik dashboard — Tiklama sayisinin otesine gecin: referrer, ulke, cihaz tipi, zamanlama grafikleri. Chart.js ile basit bir on yuz yeterli.
-
URL son kullanma tarihi —
expiresAtalani ekleyin. Gecici kampanya linkleri icin ideal. -
Kimlik dogrulama — JWT veya API key ile kullanici bazli URL yonetimi. Herkes sadece kendi linklerini gorsun.
Crawler yapimiyla da ilgileniyorsaniz, Sifirdan SEO Crawler Yapin rehberimize goz atin.
Olceklenebilir backend projeleri icin yazilim danismanligi hizmetimizi inceleyin.
Sikca Sorulan Sorular
Bu proje veritabani olmadan calisir mi?
Calisir — bu rehberdeki haliyle calisiyor. JavaScript'in Map yapisi veriyi bellekte tutar, okuma/yazma islemleri nanosaniye mertebesinde gerceklesir. Dezavantaj: sunucu kapandiginda veya yeniden basladiginda tum veriler sifirlanir. Kisisel projeler ve prototipleme icin yeterli. Uretim icin Redis veya SQLite ekleyin.
URL kisaltici guvenlik riskleri nelerdir?
En buyuk risk open redirect: saldirgan kotu amacli bir siteyi kisaltip (ornegin phishing sayfasi) masummus gibi dagitir. Bunu onlemek icin: (1) kisaltilan URL'leri Google Safe Browsing API ile kontrol edin, (2) yonlendirme oncesi ara sayfa gosterin ("Bu baglantiya yonlendiriliyorsunuz: ..."), (3) spam bildirimi mekanizmasi ekleyin. Ayrica javascript:, data: ve file: protokollerini engelleyin — bu rehberdeki isValidUrl fonksiyonu bu kontrolu zaten yapiyor.
Uretimde in-memory store yerine ne kullanmaliyim?
Kullanim senaryonuza bagli. Redis: saniyede yuz binlerce okuma/yazma, TTL (otomatik silme) destegi, en hizli secenek. PostgreSQL: karmasik sorgular, iliskisel veri, analitik raporlama gerekiyorsa. SQLite: tek sunucu, dusuk trafik, sifir bagimliilik istiyorsaniz. Cogu kucuk-orta olcekli proje icin Redis + PostgreSQL kombinasyonu idealdir: Redis onbellekte hizli yonlendirme yapar, PostgreSQL kalici depolama ve analitik saglar.
