🤞 完整範例:Github Repo    │    👻 Live Demo:點我看 demo


本專案實作略過點擊次數統計及來源訪客分析。

短網址(Short URL)是什麼?

短網址(Short URL)是將原始長網址透過 URL shortening 技術轉換為較短字串的結果,使連結更容易分享、記憶與管理。常見服務如 BitlyTinyURL

範例

🥺🥺🥺 由於本專案部署於 Vercel,並使用其提供的免費子網域作為服務域名,而該網域本身字元較長,因此短網址的整體長度也相對受到影響(所以本篇範例和 Demo 的短網址會比較長喔)。

例如使用 reurl.cc 產生的短網址長度就只有這樣:https://reurl.cc/53ZNyR

為什麼需要短網址?

  • 縮短冗長 URL,節省字元空間
  • 便於在字數受限的平台(如社群媒體、簡訊)分享
  • 提升連結外觀整潔度與可讀性
  • 可追蹤點擊次數、來源、地理位置等數據
  • 可自訂短網址 slug,強化品牌辨識
  • 隱藏原始網址中的參數與路徑結構
  • 部分服務支援設定連結到期時間,控制存取期限

短網址的實際運作流程

當使用者點擊一個短網址時,系統背後其實會經歷一連串非常快速的查詢與轉址流程。從使用者的角度來看只是一次點擊,但在系統內部則包含「識別 → 查詢 → 重新導向」三個核心步驟。

整體流程可拆解如下:

1. 使用者請求短網址

使用者在瀏覽器輸入或點擊短網址:https://url-shortener-doe.vercel.app/twLgDN ,此時請求會被送往短網址服務的伺服器。這類伺服器通常是邊緣節點(Edge server)或 API server。

2. 系統解析短碼(Short Code)

伺服器會從 URL path 中解析出短碼:twLgDN,這個短碼是整個系統的核心 key。

3. 查詢對應的原始網址(Original URL)

系統會透過 key-value lookup 查詢儲存層,例如:Redis、Database、Cache layer。

GET twLgDNhttps://www.google.com/maps/place/Taipei+101+Shopping+center/@25.0341017,121.5642863,19z/data=!3m1!5s0x3442abb6d95eb43b:0xbfc3edc9e6aa3050!4m6!3m5!1s0x3442abb6da80a7ad:0xacc4d11dc963103c!8m2!3d25.0341222!4d121.5640212!16s%2Fg%2F11fx91ft3n?entry=ttu

如果存在則回傳原始網址;如果不存在則回傳 404 或錯誤頁面。

4. 執行 HTTP Redirect

當查詢成功後,系統會回傳 HTTP redirect response,瀏覽器收到後會自動導向原始網址。

5. 使用者被導向目標網站

從使用者的角度來看很簡單:點擊一個短連結,接著被帶到目標頁。但實際上,這個過程背後已經完成一次完整的 lookup + routing process。

延伸補充:邊緣運算(Edge Computing)在短網址中的角色

隨著技術演進,現代大型短網址服務通常會搭配邊緣運算(Edge Computing)架構,以降低轉址延遲(Redirect Latency)並提升存取效能。

其主要角色包括:

  • 極致的跳轉速度: 透過全球分佈的邊緣節點,短網址的「解析」與「跳轉」將會在離使用者最近的伺服器完成,將延遲降至最低。
  • 動態分流與導向: 在邊緣端直接判斷使用者的裝置、語言或地理位置,即時決定要跳轉到哪個目標頁面,無需回傳至遠端中心伺服器處理。
  • 第一線安全過濾: 在邊緣節點即時攔截惡意機器人(Bots)或阻斷服務攻擊(DDoS),在威脅到達主系統前就先進行清洗與防護。
  • 即時數據預處理: 點擊數據在邊緣端先進行去識別化或初步匯整,提升後續大數據分析的效率與隱私安全性。

💡 不過需要注意的是:

Edge Computing 並不是短網址服務的必要條件。對於小型專案而言,單一 API server 搭配 Redis 通常已足夠。 然而隨著 Serverless 與 Edge 平台的普及,即使是小型專案,也能透過 Vercel Edge Functions、Cloudflare Workers 或 Upstash Redis 等服務,以較低成本實現低延遲的全球存取能力。

系統架構設計

整體架構圖

(TODO)

Edge Computing 在短網址中的角色

短網址服務對於延遲(Latency)相當敏感,因為每一次短網址存取都需要即時完成重新導向(Redirect)。假設所有請求都集中回傳至單一的後端伺服器處理,當使用者與伺服器距離較遠時,可能會增加重新導向的時間並影響使用體驗。

Edge Computing 的核心概念,是將請求處理邏輯部署到更靠近使用者的邊緣節點(Edge Nodes),降低請求的往返距離與網路延遲。

因此在短網址系統中,Edge 通常負責接收短網址請求、解析短網址、查詢 URL 映射及快速回傳 HTTP redirect。

為什麼選擇 Redis

在面對短網址服務這種「高併發讀取、結構極其簡單」的應用場景時,傳統的關聯式資料庫往往顯得過於沉重,而 Redis 則非常符合所有需求:

  • Key-Value 結構完美契合:短網址的本質就是極其單純的鍵值對(Key-Value)對照。Short Code (Key) → Original URL (Value) Redis 內建的 String 結構提供了 O(1) 的查詢時間複雜度,能實現微秒(mu)等級的記憶體讀取速度,這是一般 SQL 資料庫做索引查詢無法比擬的。
  • 執行緒安全的原子操作(Atomic Operations):短網址服務通常需要統計點擊次數。在傳統資料庫中,高併發的 UPDATE counter = counter + 1 容易遇到資料鎖定(Locking)或競爭條件(Race Condition)問題。而 Redis 的 INCR 指令是執行緒安全的原子操作,能夠輕鬆吞吐每秒數萬次的點擊計數,完全不卡頓。
  • 內建 cache 與過期機制(TTL):許多短網址(例如行銷活動連結)具有時效性。Redis 內建的 EXPIRE 機制允許我們直接為每個短網址設定過期時間(Time-To-Live,簡稱為 TTL)。時間一到,資料會自動從記憶體中抹除,省去了寫排程去定期清理舊資料的麻煩。
  • 進階資料結構的彈性:除了單純的網址映射,Redis 還提供了 List、Hash 與 Set 等結構。這讓我們在後續擴充功能時,可以輕鬆地實作像是「限制同一 IP 的點擊頻率」或是「只保留最近 5 筆短網址紀錄」等進階功能。

Serverless Architecture

在現代 Web 開發中,維護一台 24 小時開機、需手動調整規格的傳統伺服器,不論在成本還是維運上都是負擔。短網址服務天然具備「流量不規律」的特性(例如行銷活動簡訊發出瞬間流量暴增,平時則極低),而這正是 Serverless 架構的優勢所在:

  • 解決傳統資料庫的 Connection Pool 問題: 在 Serverless 環境中(如 Vercel Functions),每一次請求進來都會觸發一個獨立的容器實例。當瞬間湧入萬人點擊時,會同時啟動上萬個實例,這會導致傳統資料庫的 Connection Pool 瞬間爆滿而崩潰。
  • Upstash HTTP/REST 連線的絕佳優勢: Upstash Redis 專為 Serverless 設計,它不使用傳統持久性的 TCP 連線,而是透過 HTTP/REST API 進行操作。這意味著無論前端併發請求有多高,都不會發生連線耗盡(Connection Exhaustion)的錯誤。
  • 自動彈性伸縮(Auto-scaling): Serverless 架構讓開發者不需要擔心主機當掉或流量爆掉。在沒有流量時,基礎設施成本為零(Pay-as-you-go);當爆發性流量進來時,Vercel 與 Upstash 會在毫秒內自動擴展,承接所有請求,實現真正的免運維(Ops-less)。

Tech Stack

  • Next.js - Full Stack Framework 提供 Fullstack 開發能力,整合 API Routes、App Router 與 Edge Runtime,適合建構具備前後端整合能力的短網址服務。
  • Upstash Redis - Redis 為記憶體型 Key-Value Database,具備極低延遲(Low Latency)與 O(1) 查詢特性,非常適合 redirect-heavy 的短網址查詢場景。Upstash 同時提供 Serverless 架構,適合部署於 Vercel。
  • Nano ID - 用於生成短網址代碼(Short Code),具備短字串、低碰撞率與 URL-safe 特性,適合作為短網址識別碼。
  • TypeScript - 提供靜態型別檢查,提升程式可維護性與開發時的型別安全。
  • Vercel - 提供 Serverless Deployment 與全球 Edge Network,可降低 redirect latency,並與 Next.js 深度整合。

短碼(Short Code)生成策略

本專案的短碼生成採用:Nano ID

Nano ID 是目前相當流行的隨機 ID 生成工具,常見於短網址服務、分散式系統、前後端唯一識別碼生成及 Serverless/Edge 應用等。

其核心概念是:使用高品質亂數產生短字串,例如:twLgDNa8KxP2Qw91Zm

為什麼選擇 Nano ID?

相較於傳統的 Auto Increment ID,Nano ID 有幾個非常適合短網址服務的特性。

1. 短碼不可預測

若使用遞增 ID:/1/2/3,攻擊者很容易進行枚舉攻擊(Enumeration Attack),也就是暴力猜測所有短網址;而 Nano ID 使用隨機生成:/twLgDN/a8KxP2。由於短碼之間沒有明顯規律,因此能達到有效降低私有連結被猜測、避免爬蟲批量掃描、防止資料外洩等的目的。

2. 適合分散式與 Serverless 架構

Nano ID 的核心優勢在於完全去中心化。它不依賴資料庫自動遞增(Auto Increment)、中央發號器或單一機器序號,因此任何 Edge Function 或 Serverless 實例都能在本地端獨立生成唯一短碼。

這種特性讓它成為 Vercel Edge Functions、Serverless API 以及多區域(Multi-region)部署的絕佳選擇。由於生成過程中無需向中央資料庫「預先索取 ID」,不僅大幅降低了網路延遲,也實現了低系統耦合度。

3. 短碼長度可自訂

例如以下可生成 6 位短碼:

nanoid(6);

若需要更低的碰撞率,可增加短碼長度,生成更多位數的短碼:

nanoid(8);

碰撞率(Collision Probability)

Nano ID 雖然是隨機生成,但理論上仍可能發生 collision(碰撞),即兩個網址產生相同短碼(不過實務上機率極低)。

假設當使用 Base62 字元集、長度為 6 時,此時總組合數約為 62^6^ ≈ 5.68×10^10,大約 568 億種組合。

因此在一般的中小型短網址服務中,碰撞率通常可以被忽略。但若系統規模進一步擴大,則可以透過增加短碼長度、collision retry 機制或者 database unique constraint 等的方法進一步降低風險。

Nano ID 的實作

import { nanoid } from 'nanoid';

const shortCode = nanoid(6);

生成結果

twLgDN

接著系統會將 shortCode → Original URL 這個 key-value 映射儲存至資料庫。當使用者請求 /twLgDN 時,系統即可查詢對應原始網址並重新導向。

自訂短碼

除了自動生成外,部分短網址服務也支援使用者自訂短碼。例如:/summer-sale/black-friday 等。

自訂短碼的優點包含更容易記憶、適合品牌行銷及提升可讀性等;但系統需額外處理例如名稱衝突、敏感詞過濾等的問題。

本專案主要仍以 Nano ID 自動生成為主。

短網址生成流程實作

在 Next.js 的 App Router 中,會在 app/api/shorten/route.ts 建立一個 POST 路由來處理短網址的生成。

URL Validation

在將網址存入資料庫之前,先進行驗證:

function isValidUrl(value: string): boolean {
  try {
    const url = new URL(value);
    return url.protocol === 'http:' || url.protocol === 'https:';
  } catch (e) {
    return false;
  }
}

const vError = getValidationError(url);
if (vError) {
  return setValidationError(vError);
}

生成短碼

這裡採用前面提到的 Nano ID,並限制長度為 6 碼:

import { nanoid } from 'nanoid';

const shortCode = nanoid(6); // 例如: "twLgDN"

把高併發的環境考慮進去,單純依賴隨機性存在著微小的碰撞風險--即兩個不同的使用者在同一毫秒內,隨機生成了完全相同的短碼(呃雖然機率極小)。

但為了達成 100% 的唯一性保證,還是採取了先預佔、再確認的方式:

export async function generateUniqueCode(length: number = 6): Promise<string> {
  let attempts = 0; // 當前重試次數
  const maxAttempts = 5; // 重試次數上限

  while (attempts < maxAttempts) {
    // 1. 產生隨機短碼
    const shortCode = nanoid(length);

    /**
     * 2. 原子性預佔 (Atomic Reservation):
     * 利用 Redis 的 'SET' 指令搭配 NX 與 EX 選項,實作輕量級的分散式鎖
     *
     * - `reserved:${shortCode}`: 專門的命名空間,隔離預佔鎖與實際資料
     * - '1': 佔位用的任意值,可替換成其他值,我們只關心這個 Key 存不存在
     * - nx: true (Set if Not Exists): 核心原子操作。只有當 Key 不存在時才能寫入成功
     * - ex: 60 (Expire): 設定 60 秒的防死鎖 TTL。若後續寫入程式崩潰,此 ID 仍會自動釋放
     */
    const reserved = await redis.set(`reserved:${shortCode}`, '1', {
      nx: true,
      ex: 60,
    });

    // 3. 預佔成功,代表此 ID 目前無人使用,可回傳該值
    if (reserved === 'OK') {
      return shortCode;
    }

    // 4. 發生碰撞,計數器加 1 並重新嘗試
    attempts++;
    console.warn(
      `Collision detected for code: ${shortCode}. Retrying... (${attempts}/${maxAttempts})`
    );
  }

  // 5. 達到重試上限時的防禦性錯誤拋出
  throw new Error(
    'Collision threshold reached: Could not generate a unique short code.'
  );
}

寫入 Redis

使用 Upstash Redis:

import { Redis } from '@upstash/redis';

const redis = Redis.fromEnv();

await redis.set(shortCode, originalUrl);

在這裡為了優化使用者體驗,使用 Redis Pipeline 一次執行兩個操作:

  1. 寫入網址映射 Short Code (Key) → Original URL (Value)
  2. 並同步將此筆紀錄推入最近生成的 List(Capped Collection)中(這個 List 是為了在網站上列出最近生成的幾筆資料以供 demo 檢視)

使用 Redis Pipeline 寫入 Upstash Redis:

import { NextResponse } from 'next/server';
import { Redis } from '@upstash/redis';
import { generateUniqueCode } from '@/lib/utils';

const SHORT_URL_EXPIRY_DAYS = 3; // 設定短網址有效期為 3 天
const redis = Redis.fromEnv();

export async function POST(req: Request) {
  try {
    const { url } = await req.json();
    if (!url)
      return NextResponse.json({ error: 'URL is required' }, { status: 400 });

    // 1. 生成高併發防碰撞的唯一短碼 (例如: "twLgDN")
    const shortCode = await generateUniqueCode();

    /**
     * 2. 儲存網址映射關係 (Short Code -> Original URL)
     * - 'ex': 設定 3 天的生存時間 TTL (以秒為單位換算)。
     * - 超過 3 天後,Redis 會自動從記憶體中剔除此 Key,避免資料無限膨脹。
     */
    await redis.set(shortCode, url, {
      ex: SHORT_URL_EXPIRY_DAYS * 24 * 60 * 60,
    });

    /**
     * 3. 更新全域「最新 5 筆縮網址」紀錄清單
     * 這裡使用 Redis Pipeline 將多個指令打包一次發送。
     * 這樣能大幅減少 Serverless 環境與 Redis 之間的網路往返次數 (RTT),並確保操作的連續性。
     */
    const tx = redis.pipeline();
    tx.lpush('recent_shortens', shortCode); // 將新短碼推入清單最左側
    tx.ltrim('recent_shortens', 0, 4); // 永遠只保留索引 0 到 4 (前 5 筆)
    await tx.exec(); // 一次執行 Pipeline 中的所有指令

    // 4. 成功回傳短碼
    return NextResponse.json({ shortCode });
  } catch (error) {
    console.error('Shorten API Error:', error);
    return NextResponse.json({ error: 'Server Error' }, { status: 500 });
  }
}

取得短網址

生成與儲存成功後,前端拿到的 shortCode 需組合成完整的短網址:

const shortUrl = `${window.location.origin}/${shortCode}`;

Redirect 實作流程

為了讓使用者點擊短網址後能以最快速度跳轉,這裡不採用傳統的動態頁面路由(如 app/[id]/page.tsx),而是利用 Next.js 的 Middleware。Middleware 運行於 Edge Runtime,能在 HTTP 請求到達伺服器主體前進行攔截,將重新導向的延遲降至最低。

Dynamic Route Handling

在系統設計中,首頁(/)、API 路由(/api/*)以及 Next.js 內建的靜態檔案(/_next/*favicon.ico)不應該被當作短網址進行解析。

透過 Matcher 配置條件過濾 來處理動態路徑:

// 設定 Matcher 確保效能,利用正規表示式初步過濾掉靜態檔案與 API
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

並在 middleware.ts 進行攔截:

const path = req.nextUrl.pathname;

// 過濾不需要攔截的路徑 (首頁、API、靜態檔案、帶有點副檔名的檔案)
if (
  path === '/' ||
  path.startsWith('/api') ||
  path.startsWith('/_next') ||
  path.includes('.')
) {
  return NextResponse.next();
}

// 提取短碼 (例如從路徑 "/abc" 中分離出 "abc")
const shortCode = path.split('/')[1];

這樣一來,只有真正符合短網址特徵的請求(例如:yourdomain.com/twLgDN)才會觸發後續的查詢,確保服務的核心流量不會被不必要的檢查拖慢。

Redis Query

當確定請求路徑為短碼後,Middleware 會直接在邊緣節點向 Upstash Redis 發起異步查詢。

// 在邊緣節點直接查詢 Redis,時間複雜度為 O(1)
const longUrl: string | null = await redis.get(shortCode);

💡 為什麼在這裡查詢能做到極速?

Next.js 的 Middleware 部署在 Vercel 的全球分佈式網路(CDN 邊緣)上。配合 Upstash Redis 的 Global Database(全球同步)特性,不論使用者是在台灣、美國還是全球其他地方點擊連結,Middleware 都會向物理距離最近的 Redis 節點讀取資料。這大大降低了跨國網路回傳的巨大延遲。

Redirect Response

如果 Redis 成功回傳了原始的長網址,便能立即回傳 HTTP 的 Redirect 進行重新導向:

// 4. 若找到原始網址,執行重新導向
if (longUrl) {
  return NextResponse.redirect(new URL(longUrl, req.url), {
    status: 307,
  });
}

💡 為什麼這裡的 redirect 使用 307?

在 HTTP 規範中,雖然 301、302 和 307 都能達到網址跳轉的效果,但 307 具備了短網址服務最核心的兩大優勢:

  • 完全 No Cache,確保點擊數據精確度 與會被瀏覽器強力 cache 的 301(永久導向) 不同,307 告訴瀏覽器這只是一個「臨時」的中轉站。這意味著每一次使用者點擊該短網址時,瀏覽器都必須乖乖回到我們的 Middleware 詢問新網址。這對於未來想要擴充「統計點擊次數」、「分析訪客來源(IP、國家、裝置)」等數據分析功能至關重要。 本專案實作略過點擊次數統計及來源訪客分析。
  • 嚴格保留原始 HTTP 請求 Method 這是 307 用來解決傳統 302(暫時導向) 的關鍵。在舊規範的 302 下,如果原本的請求是帶有資料的 POST,部分瀏覽器在重新導向時會自作聰明地將其篡改並降級為 GET,導致資料遺失或請求出錯。 而 307 則強制規定瀏覽器「絕對不能」變更請求方法。如果外部服務透過 POST 或 PATCH 呼叫你的短網址,收到 307 後,瀏覽器會確保帶著原本的 Method 與 Payload 安全地重新導向至目標長網址。

Error Handling

如果短網址在 Redis 中找不到,這時候就會進入錯誤處理。在實務上,短網址失敗通常有以下兩種情境,我們需要針對它們進行優化設計:

1. 不合法的惡意代碼或打錯字(Invalid Code)

當使用者不小心打錯字,或是機器人惡意掃描不存在的短碼時,Redis 會回傳 null

// 若找不到,則交給 Next.js 後續路由,最終觸發內建的 404 頁面
return NextResponse.next();

2. 過期的短網址(Expired Code)

我在「寫入 Redis」章節中設定了 3 天的 TTL (Time-To-Live)。 當一個短網址建立了超過 3 天,Upstash Redis 就會自動將該 Key 徹底抹除。對 Middleware 而言,過期網址的表現跟「從未存在過的不合法代碼」是一模一樣的,Redis 同樣都會回傳 null

當然,如果想在實務的專案中區分「打錯字」與「已過期」,可以考慮改用 Redis 的 Hash 結構,內部記錄 expiredAt 時間戳,並在 Middleware 中與當前時間進行比較。如果過期,則轉而重新導向至一個顯示「該連結已過期」的提示頁面,從而增加使用者的體驗。

本專案實作先略過連結過期的提示頁面。

結論

打造現代短網址服務,本質上是將邊緣運算(Edge)與記憶體資料庫(Redis)結合的過程。核心可歸納為以下四點:

1. 核心本質:極簡的 Key-Value 系統

短網址底層就是單純的 Key-Value 映射。捨棄複雜的 SQL 關聯式資料庫,回歸最扁平的鍵值對設計,在高併發架構下將效能最佳化。

2. 讀取優化:Redis 有效支撐高併發下的計數操作

短網址屬於極端的「讀多寫少」場景。Redis 的 O(1) 記憶體查詢速度、執行緒安全的原子操作(如 INCR 計數),以及內建的 EXPIRE(TTL)空間回收機制,是應對突發流量的最優解方。

3. 極致體驗:Edge Runtime 消除全球延遲

透過 Next.js 的 Edge Middleware 在 CDN 邊緣節點直接攔截請求,並搭配 Upstash 的全球同步資料庫,將傳統跨國網路的往返時間(RTT)降到最低,讓全球使用者能感受到無時間差的重新導向。

4. Serverless 架構的優勢與限制

  • 優勢: 免運維且成本彈性。特別是 Upstash 的 HTTP/REST 連線,徹底解決了 Serverless 實例瞬間暴增導致資料庫 Connection Pool 崩潰的問題。
  • 限制: Edge Runtime 環境精簡,不適合執行重型運算。因此架構上必須「職責分離」——邊緣節點只負責核心的「攔截與轉導」,複雜的數據分析則應交由後台非同步處理。

透過 Next.js + Upstash Redis,用極少的 code 與近乎零的維運成本,實現了高可用、低延遲的現代化 Web 架構。

🚀 完整範例 & Demo

本文的實作範例可由以下查閱:

🤞 完整範例:Github Repo    │    👻 Live Demo:點我看 demo