LinkShort
Live: https://link-shortener.dev2k.org
Was es macht
Ein URL-Shortener mit Analytics-Dashboard und Feedback-Widget. Nutzer kürzen lange URLs, sehen Klick-Statistiken pro Link (letzte 7/30 Tage) und können direkt im Interface Feedback senden.
Tech-Stack: Node.js (natives node:http) · PostgreSQL · Vanilla JS · Hosting: Render · CI/CD: GitHub Actions
Entwicklungsansatz
- Strukturierte KI-Prompts mit Kontext und Constraints - Code bleibt konsistent mit dem gewählten Stack, kein Blind-Trust in generierte Ausgaben
- TDD (Tests vor der Implementierung) - Regressions in Production mehrfach verhindert, Coverage als Qualitätssignal nicht als Pflicht
- Dependency Injection für alle externen Services - vollständig ohne echten Netzwerkzugriff testbar
KI-gestützt entwickelt mit Claude Code - mit Review-Mindset, nicht als Shortcut
Features
- Create short links with optional custom slug
- AI-generated short descriptions for stored URLs
- Click tracking with referrer, user-agent, and bot detection
- Analytics API per link: period timeline, referrers, and device distribution
- Dashboard: overview stats, clicks per day, top links, referrer breakdown (auth required)
- JWT authentication (register/login)
- Rate limiting per IP, security headers, input validation
- Batch automation for missing descriptions
- Automated PR review via GitHub Actions + Claude API
- Feedback widget on all pages (no auth required), with email notification via Resend
Planned
- Redis caching for redirect endpoints (Cache-Aside pattern, TTL-based, fallback to DB)
Installation
# 1. Clone & install
git clone https://github.com/KosMaster87/link-shortener.git && cd link-shortener
npm install
# 2. Create PostgreSQL database
createdb linkshort
# 3. Apply schema
psql linkshort < src/db/schema.sql
psql linkshort < src/db/migrations/002_add_users.sql
psql linkshort < src/db/migrations/003_add_description.sql
psql linkshort < src/db/migrations/004_add_feedback.sql
# 4. Configure environment
cp .env.example .env
# Generate a JWT_SECRET:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Paste the output as JWT_SECRET in .env
# 5. Start
npm start
Server runs on http://localhost:3000.
Configuration
Copy .env.example to .env:
PORT=3000
NODE_ENV=development
# Option A (production)
# DATABASE_URL=postgresql://user:pass@ep-xxxx-pooler.eu-central-1.aws.neon.tech/linkshort?sslmode=require&channel_binding=require
# USE_DATABASE_URL=true
# Option B (local/alternative)
PGHOST=/var/run/postgresql
PGPORT=5432
PGDATABASE=linkshort
PGUSER=your-local-pg-user
PGPASSWORD=
USE_DATABASE_URL=false
# Required - generieren: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
JWT_SECRET=
SESSION_EXPIRY=86400
# Email notifications (optional - skip to disable)
RESEND_API_KEY=
FROM_EMAIL=
TO_EMAIL=
# Optional - benötigt für scripts/batch-describe.js und PR-Review-Workflow
ANTHROPIC_API_KEY=
LOG_LEVEL=info
RATE_LIMIT_MAX=100
Lokale Entwicklung vs. Production DB
DATABASE_URL wird nur genutzt wenn USE_DATABASE_URL=true gesetzt ist.
Lokal greift standardmäßig die Unix-Socket-Konfiguration über die PG*-Variablen.
| Umgebung | USE_DATABASE_URL | Genutzte Verbindung |
|---|---|---|
| Lokal | false | PGHOST, PGUSER, PGDATABASE (Unix-Socket) |
| CI | false | PGHOST, PGUSER, PGDATABASE (TCP, aus Workflow-Secrets) |
| Render | true | DATABASE_URL (Neon PostgreSQL, gepoolte URL mit -pooler) |
Hinweis: Ist
DATABASE_URLgesetzt undUSE_DATABASE_URL=false, erscheint eine Warnung im Server-Log. Das ist kein Fehler - der Server nutzt trotzdem die lokale DB.
Neon lokal testen (bewusst, nicht Standard):
USE_DATABASE_URL=true npm start
Render + Neon (Empfohlen)
- In Render
DATABASE_URLals Secret setzen (Neon Connection String) - In Render
USE_DATABASE_URL=truesetzen - Für Pooling den
-poolerHost von Neon nutzen - Passwort bleibt identisch, nur der Hostname ändert sich
Für die vollständige Umstellung inkl. Datenmigration siehe NEON_MIGRATION_RUNBOOK.md.
Incident Quickcheck (3 Min)
- Beide Health-URLs prüfen:
/healthaufonrender.comunddev2k.orgmüssen200liefern. - Render-Logs prüfen: bei DB-Fehlern (
database removed,connection terminated) sofort Incident notieren. - Render-Environment prüfen:
DATABASE_URLgesetzt undUSE_DATABASE_URL=true. - Neon prüfen: aktiver Endpoint/Branch, Compute startbar, Connection-String unverändert.
- Nach Fix verifizieren: beide Monitore wieder grün, sonst Manual Redeploy auslösen.
One-liner (beide Health-Checks, mit PASS/FAIL + Exit-Code):
ok=1; for u in https://link-shortener-h40z.onrender.com/health https://link-shortener.dev2k.org/health; do c=$(curl -sS -o /tmp/health.out -w "%{http_code}" "$u") || c=000; printf "\n== %s ==\nHTTP %s\n" "$u" "$c"; cat /tmp/health.out; echo; [[ "$c" == "200" ]] || ok=0; done; [[ $ok -eq 1 ]] && echo "ALL HEALTH CHECKS PASS" || { echo "HEALTH CHECK FAILED"; exit 1; }
ANTHROPIC_API_KEY is required for scripts/batch-describe.js and the automated PR review workflow.
Testing
npm test
npm test startet den lokalen Server bei Bedarf automatisch (Port 3000),
fuehrt die komplette Suite aus und beendet den Server danach wieder.
Falls du die Suite ohne diesen Helper direkt laufen lassen willst:
npm run test:raw
Requires a running PostgreSQL instance with the linkshort database.
Documentation
- Manuelle Projektdokumentation:
docs/manual/ - Docusaurus + API-Referenz (TypeDoc aus JSDoc-Kommentaren):
docs-site/
# Doku lokal entwickeln
npm run docs:dev
# Doku-Build erstellen
npm run docs:build
# Gebaute Doku lokal ausliefern
npm run docs:serve
API
Health
| Method | Path | Response |
|---|---|---|
| GET | /health | 200 { status: "ok", ... } or 503 on DB failure |
Auth
| Method | Path | Body | Response |
|---|---|---|---|
| POST | /api/auth/register | { email, password } | { token, user } |
| POST | /api/auth/login | { email, password } | { token, user } |
Links
All write operations require Authorization: Bearer <token>.
| Method | Path | Body / Params | Response |
|---|---|---|---|
| GET | /api/links | - | Array of links with description |
| POST | /api/links | { url, slug? } | Created link |
| PUT | /api/links/:code | { url } | Updated link |
| PATCH | /api/links/:code/toggle | - | Toggled link |
| DELETE | /api/links/:code | - | 204 No Content |
| GET | /:code | - | 302 Redirect |
Each link object includes code, originalUrl, description, createdAt, isActive, and userId.
Dashboard
All dashboard endpoints require Authorization: Bearer <token>.
| Method | Path | Query Params |
|---|---|---|
| GET | /api/dashboard/overview | - |
| GET | /api/dashboard/top-links | limit (1-100) |
| GET | /api/dashboard/clicks-per-day | days (1-365) |
| GET | /api/dashboard/referrer/:code | - |
Feedback
| Method | Path | Body | Auth |
|---|---|---|---|
| POST | /api/feedback | { type, description, email? } | none |
type: bug | improvement | other. Returns 201 { message } on success.
Analytics
| Method | Path | Query Params |
|---|---|---|
| GET | /api/links/:code/clicks | - |
| GET | /api/links/:code/clicks/period | period (day|week|month) |
| GET | /api/links/:code/referrers | - |
| GET | /api/links/:code/devices | - |
Project Structure
link-shortener/
├── server.js
├── render.yaml
├── scripts/
│ ├── batch-describe.js
│ └── pr-review.js
├── .claude/
│ └── commands/
│ └── deploy-check.md
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── pr-review.yml
├── docs/
│ └── manual/
│ ├── architecture/
│ ├── decisions/
│ ├── how-to/
│ └── runbooks/
├── docs-site/
│ ├── docusaurus.config.js
│ ├── docs/
│ │ └── api/ # generiert via TypeDoc
│ └── build/ # generierter Static Build
├── src/
│ ├── config.js
│ ├── db/
│ │ ├── index.js
│ │ ├── schema.sql
│ │ └── migrations/
│ │ ├── 002_add_users.sql
│ │ └── 003_add_description.sql
│ ├── middleware/
│ │ └── auth.js
│ ├── routes/
│ │ ├── auth.js
│ │ ├── links.js
│ │ ├── redirect.js
│ │ ├── analytics.js
│ │ ├── dashboard.js
│ │ └── feedback.js
│ ├── services/
│ │ ├── auth-service.js
│ │ ├── link-service.js
│ │ ├── analytics-service.js
│ │ ├── dashboard-service.js
│ │ └── email-service.js
│ └── utils/
│ ├── device-classifier.js
│ ├── jwt.js
│ ├── rate-limit.js
│ ├── result.js
│ └── validators.js
├── public/
│ ├── index.html
│ ├── login.html
│ ├── dashboard.html
│ ├── app.js
│ ├── style.css
│ └── feedback-widget.js
├── tests/
│ ├── analytics-devices.test.js
│ ├── analytics-period.test.js
│ ├── analytics-referrers.test.js
│ ├── link-service.test.js
│ ├── analytics-service.test.js
│ ├── auth-service.test.js
│ ├── dashboard-auth.test.js
│ ├── e2e-redirect.test.js
│ ├── feedback.test.js
│ └── email-service.test.js
├── .env.example
├── package.json
└── README.md
Automation
scripts/batch-describe.jsgenerates missing URL descriptions for rows wheredescription IS NULL.github/workflows/pr-review.ymlruns an automated Claude-based PR review for internal pull requestsscripts/pr-review.jsbuilds the review comment and updates the existing bot comment instead of posting duplicates
Developer
Konstantin Aksenov GitHub: https://github.com/KosMaster87 Email: Konstantin.Aksenov@dev2k.org Portfolio: https://portfolio.dev2k.org