Skip to content

Repository files navigation

LinkShort

CI Node.js

Ein URL-Shortener mit Analytics-Dashboard und Feedback-Widget. Node.js (natives node:http), PostgreSQL, Vanilla JS.


Live

Environment URL
App link-shortener.dev2ksoftware.com
Docs link-shortener-docu-saurus.onrender.com

Hosting: Render (Node-Service + Docusaurus-Static-Service) · DB: Neon PostgreSQL · 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


Core Features

  • Create short links with optional custom slug
  • AI-generated short descriptions for stored URLs (OpenRouter, kostenlose Modelle)
  • 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 + OpenRouter
  • 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)

Quick Start / Local Development

# 1. Clone & install
git clone https://github.com/KosMaster87/Link-Shortener-Render.git && cd Link-Shortener-Render
pnpm 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
pnpm 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 bin/describe-url.js, scripts/batch-describe.js und PR-Review-Workflow
OPENROUTER_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_URL gesetzt und USE_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 pnpm start

Render + Neon (Empfohlen)

  • In Render DATABASE_URL als Secret setzen (Neon Connection String)
  • In Render USE_DATABASE_URL=true setzen
  • Für Pooling den -pooler Host 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)

  1. Beide Health-URLs prüfen: /health auf onrender.com und dev2ksoftware.com müssen 200 liefern.
  2. Render-Logs prüfen: bei DB-Fehlern (database removed, connection terminated) sofort Incident notieren.
  3. Render-Environment prüfen: DATABASE_URL gesetzt und USE_DATABASE_URL=true.
  4. Neon prüfen: aktiver Endpoint/Branch, Compute startbar, Connection-String unverändert.
  5. 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.dev2ksoftware.com/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; }

OPENROUTER_API_KEY is required for bin/describe-url.js, scripts/batch-describe.js and the automated PR review workflow.

Testing

pnpm test

pnpm 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:

pnpm run test:raw

Requires a running PostgreSQL instance with the linkshort database.

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

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 -

Automation

  • scripts/batch-describe.js generates missing URL descriptions for rows where description IS NULL
  • .github/workflows/pr-review.yml runs an automated OpenRouter-based PR review for internal pull requests
  • scripts/pr-review.js builds the review comment and updates the existing bot comment instead of posting duplicates

Documentation

  • Manuelle Projektdokumentation: docs/manual/
  • Docusaurus + API-Referenz (TypeDoc aus JSDoc-Kommentaren): docs-site/
# Doku lokal entwickeln
pnpm run docs:dev

# Doku-Build erstellen
pnpm run docs:build

# Gebaute Doku lokal ausliefern
pnpm run docs:serve

License

No project license file is currently included.

Developer

Konstantin Aksenov GitHub: https://github.com/KosMaster87 Email: konstantin@dev2ksoftware.com Portfolio: https://portfolio.dev2ksoftware.com

About

Built with Node.js and PostgreSQL, I applied analytical problem-solving to implement secure URL shortening, click tracking, and dashboard insights.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages