<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 胡亚洲，huhu</title>
    <description>The latest articles on DEV Community by 胡亚洲，huhu (@97wow).</description>
    <link>https://dev.to/97wow</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3797508%2F2b47aa50-9dcf-49db-934c-9703a6dfc982.png</url>
      <title>DEV Community: 胡亚洲，huhu</title>
      <link>https://dev.to/97wow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/97wow"/>
    <language>en</language>
    <item>
      <title>Building a modern URL shortener with Next.js — lessons from shipping y.hn</title>
      <dc:creator>胡亚洲，huhu</dc:creator>
      <pubDate>Sat, 28 Feb 2026 04:17:07 +0000</pubDate>
      <link>https://dev.to/97wow/building-a-modern-url-shortener-with-nextjs-lessons-from-shipping-yhn-47bp</link>
      <guid>https://dev.to/97wow/building-a-modern-url-shortener-with-nextjs-lessons-from-shipping-yhn-47bp</guid>
      <description>&lt;p&gt;Last month I shipped &lt;a href="https://y.hn" rel="noopener noreferrer"&gt;y.hn&lt;/a&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Next.js 16 (App Router) → Vercel Edge
Prisma ORM → Neon Postgres (serverless)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No Redis, no queue, no separate API server. One repo, one deploy target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this stack?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js App Router&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prisma&lt;/strong&gt; — 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feature Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Short link creation &amp;amp; redirect
&lt;/h3&gt;

&lt;p&gt;The core flow:&lt;/p&gt;

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

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

&lt;h3&gt;
  
  
  QR Code Generation
&lt;/h3&gt;

&lt;p&gt;Server-side QR generation using &lt;code&gt;qrcode&lt;/code&gt; library. Nothing fancy, but I added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SVG and PNG output&lt;/li&gt;
&lt;li&gt;Customizable colors&lt;/li&gt;
&lt;li&gt;Download as file&lt;/li&gt;
&lt;li&gt;Embedded logo option&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The QR codes are generated on-demand, not stored. They're deterministic, so caching headers do the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  UTM Builder
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  API
&lt;/h3&gt;

&lt;p&gt;REST API with API key auth. Simple endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a short link&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://y.hn/api/links &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://example.com", "slug": "my-link"}'&lt;/span&gt;

&lt;span class="c"&gt;# Get link stats&lt;/span&gt;
curl https://y.hn/api/links/my-link/stats &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rate limited with a simple token bucket in middleware. No external rate limiting service needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Links
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Moderation: 3 Layers of Defense
&lt;/h2&gt;

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

&lt;p&gt;&lt;strong&gt;Layer 1: Blocklist&lt;/strong&gt;&lt;br&gt;
A curated list of known malicious domains. Checked on link creation. Updated regularly from public threat intelligence feeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Real-time URL scanning&lt;/strong&gt;&lt;br&gt;
On creation, the destination URL is checked against Google Safe Browsing API. If it's flagged, the link is rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: Manual review queue&lt;/strong&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;Is it perfect? No. But it catches the obvious stuff and gives me a safety net.&lt;/p&gt;

&lt;h2&gt;
  
  
  i18n: 10 Languages
&lt;/h2&gt;

&lt;p&gt;I added internationalization early, when the app was small. This was a great decision.&lt;/p&gt;

&lt;p&gt;Stack: &lt;code&gt;next-intl&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Languages: English, Chinese (Simplified &amp;amp; Traditional), Japanese, Korean, French, German, Spanish, Portuguese, Arabic.&lt;/p&gt;

&lt;p&gt;The key lesson: &lt;strong&gt;do i18n on day one&lt;/strong&gt;. Retrofitting it is painful. When your app is small, it's a few hours of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Some numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redirect latency: &lt;strong&gt;~50ms&lt;/strong&gt; at the edge (p50), &lt;strong&gt;~120ms&lt;/strong&gt; p99&lt;/li&gt;
&lt;li&gt;Time to Interactive (homepage): &lt;strong&gt;&amp;lt; 1.5s&lt;/strong&gt; on 3G&lt;/li&gt;
&lt;li&gt;Lighthouse score: &lt;strong&gt;95+&lt;/strong&gt; across the board&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Skip Prisma for the redirect hot path.&lt;/strong&gt; A raw SQL query would shave a few ms off redirect latency. Prisma is great for the CRUD pages, overkill for &lt;code&gt;SELECT url FROM links WHERE slug = $1&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add click analytics batching earlier.&lt;/strong&gt; Currently each click triggers a DB write. At scale, I'd batch these. At 1,800 clicks it doesn't matter, but it will.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up monitoring from the start.&lt;/strong&gt; I added error tracking after the first user reported a bug. Should have done it before.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Numbers So Far
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;17 registered users&lt;/li&gt;
&lt;li&gt;1,800+ total clicks&lt;/li&gt;
&lt;li&gt;Users from 15+ countries&lt;/li&gt;
&lt;li&gt;Zero downtime&lt;/li&gt;
&lt;li&gt;$0 infrastructure cost (Vercel &amp;amp; Neon free tiers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not life-changing numbers, but it works and people use it. That's the bar for a side project.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 Try it: &lt;a href="https://y.hn" rel="noopener noreferrer"&gt;y.hn&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have questions about the architecture or want to see specific code, drop a comment. Happy to share.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
