DEV Community

胡亚洲,huhu
胡亚洲,huhu

Posted on

Building a modern URL shortener with Next.js — lessons from shipping y.hn

Last month I shipped y.hn, a URL shortener built on one of the shortest domains you can get (4 characters). Here's the technical breakdown — what worked, what didn't, and what I'd do differently.

The Stack

Next.js 16 (App Router) → Vercel Edge
Prisma ORM → Neon Postgres (serverless)
Enter fullscreen mode Exit fullscreen mode

That's it. No Redis, no queue, no separate API server. One repo, one deploy target.

Why this stack?

  • Next.js App Router gives me server components, API routes, middleware, and edge runtime in one framework. The middleware is key — it intercepts short link requests at the edge before they hit the origin, so redirects are fast globally.
  • Neon is serverless Postgres. Cold starts are minimal, and the free tier is generous. I use branching for preview deployments — every PR gets its own database branch.
  • Prisma — controversial choice, I know. But for a solo developer, the type safety and migration workflow saves me time. The performance overhead is acceptable for this use case.

Feature Architecture

Short link creation & redirect

The core flow:

  1. User submits a URL → API validates → generates slug → stores in Postgres
  2. Visitor hits y.hn/{slug} → Next.js middleware checks the path → queries DB → 301/302 redirect
  3. Click event logged asynchronously (non-blocking)

The slug generation uses a custom base62 encoder on an auto-incrementing ID. Short, predictable, collision-free. Users can also pick custom slugs.

QR Code Generation

Server-side QR generation using qrcode library. Nothing fancy, but I added:

  • SVG and PNG output
  • Customizable colors
  • Download as file
  • Embedded logo option

The QR codes are generated on-demand, not stored. They're deterministic, so caching headers do the rest.

UTM Builder

A simple form that appends utm_source, utm_medium, utm_campaign etc. to the destination URL before shortening. Saves people from manually constructing UTM strings. The UI auto-previews the final URL as you type.

API

REST API with API key auth. Simple endpoints:

# Create a short link
curl -X POST https://y.hn/api/links \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "slug": "my-link"}'

# Get link stats
curl https://y.hn/api/links/my-link/stats \
  -H "Authorization: Bearer YOUR_KEY"
Enter fullscreen mode Exit fullscreen mode

Rate limited with a simple token bucket in middleware. No external rate limiting service needed.

Deep Links

For mobile apps. If you set up deep link rules, y.hn will redirect to the app on mobile (using Universal Links / App Links) and to the web URL on desktop. This is surprisingly fiddly to get right across iOS and Android, but it works.

Content Moderation: 3 Layers of Defense

This one caught me off guard. Within hours of launching, someone tried to shorten a phishing URL. Here's what I built:

Layer 1: Blocklist
A curated list of known malicious domains. Checked on link creation. Updated regularly from public threat intelligence feeds.

Layer 2: Real-time URL scanning
On creation, the destination URL is checked against Google Safe Browsing API. If it's flagged, the link is rejected.

Layer 3: Manual review queue
Links from new accounts or links that hit certain heuristics (e.g., URL contains suspicious patterns) go into a review queue. I can approve, reject, or disable them.

Is it perfect? No. But it catches the obvious stuff and gives me a safety net.

i18n: 10 Languages

I added internationalization early, when the app was small. This was a great decision.

Stack: next-intl with JSON message files. Each language has its own file. I used a mix of manual translation (for languages I speak) and careful AI-assisted translation (for others), with native speakers reviewing.

Languages: English, Chinese (Simplified & Traditional), Japanese, Korean, French, German, Spanish, Portuguese, Arabic.

The key lesson: do i18n on day one. Retrofitting it is painful. When your app is small, it's a few hours of work.

Performance

Some numbers:

  • Redirect latency: ~50ms at the edge (p50), ~120ms p99
  • Time to Interactive (homepage): < 1.5s on 3G
  • Lighthouse score: 95+ across the board

The secret? There isn't one. Server components for the heavy pages, minimal client JS, edge middleware for redirects, and Vercel's CDN for static assets.

What I'd Do Differently

  1. Skip Prisma for the redirect hot path. A raw SQL query would shave a few ms off redirect latency. Prisma is great for the CRUD pages, overkill for SELECT url FROM links WHERE slug = $1.

  2. Add click analytics batching earlier. Currently each click triggers a DB write. At scale, I'd batch these. At 1,800 clicks it doesn't matter, but it will.

  3. Set up monitoring from the start. I added error tracking after the first user reported a bug. Should have done it before.

Numbers So Far

  • 17 registered users
  • 1,800+ total clicks
  • Users from 15+ countries
  • Zero downtime
  • $0 infrastructure cost (Vercel & Neon free tiers)

Not life-changing numbers, but it works and people use it. That's the bar for a side project.


Links:

If you have questions about the architecture or want to see specific code, drop a comment. Happy to share.

Top comments (0)