tl;dr:
npm install toui-js,三個 method 涵蓋全部 v1 公開 API,零依賴。文末有完整可跑 script,行銷部門下次丟 50 條連結來、30 秒搞定。
我在維護一個叫 toui.io 的短網址服務(名字是台語「佗位(tó-uī)」,意思是「哪裡」)。原本的 REST API 已經開放了,文件用 curl 或 fetch 範例帶過——但發現每次都要手寫 header、處理 4xx/429、把回應 type 補出來很麻煩,所以這幾天把 SDK 寫了出來,剛上 npm:
npm install toui-js
這篇就是用這個 SDK 走一輪實戰:用一個促銷活動的場景,batch 建短網址 + 加社群預覽 + 活動結束拉成效報表。
你需要:
toui_ 開頭加 32 字元 hex把 API Key 放環境變數:
# .env
TOUI_API_KEY=toui_your_key_here
.env 一定要進 .gitignore。Node 20.6+ 可以用內建的 --env-file=.env 旗標跳過 dotenv。
裝套件:
npm install toui-js
# 或 pnpm add toui-js / bun add toui-js
初始化 client:
import "dotenv/config";
import { Toui } from "toui-js";
const toui = new Toui({ apiKey: process.env.TOUI_API_KEY });
就這樣,可以開工了。
情境:行銷部門給了你一份商品清單,每條都要變短網址、要加 LINE / Facebook 分享預覽。
import "dotenv/config";
import { Toui, TouiError } from "toui-js";
const toui = new Toui({ apiKey: process.env.TOUI_API_KEY });
const products = [
{
url: "https://shop.example.com/airpods-pro-2",
title: "AirPods Pro 2",
code: "airpods",
og_title: "AirPods Pro 2 限時特價 $5,990",
og_description: "原價 $7,490,週末兩天限定。主動降噪、USB-C 充電盒。",
},
{
url: "https://shop.example.com/air-fryer-japan",
title: "日本氣炸鍋",
code: "airfryer",
og_title: "日本氣炸鍋 現折 $1,200",
og_description: "4.5L 大容量,八種預設模式,少油更健康。",
},
{
url: "https://shop.example.com/robot-vacuum-x1",
title: "掃地機器人 X1",
code: "vacuum",
og_title: "掃地機器人 X1 直降 $3,000",
og_description: "雷射導航、自動集塵、App 遠端遙控。",
},
];
async function main() {
console.log("Creating short URLs for flash sale...\n");
for (const p of products) {
try {
const link = await toui.shorten({
url: p.url,
title: p.title,
custom_code: p.code,
og_title: p.og_title,
og_description: p.og_description,
});
console.log(`${p.title}: ${link.short_url}`);
} catch (err) {
if (err instanceof TouiError) {
console.error(`✗ ${p.title} (${err.status}): ${err.message}`);
} else {
throw err;
}
}
}
console.log("\nDone! All links ready.");
}
main();
跑起來:
Creating short URLs for flash sale...
AirPods Pro 2: https://toui.io/airpods
日本氣炸鍋: https://toui.io/airfryer
掃地機器人 X1: https://toui.io/vacuum
Done! All links ready.
幾個重點:
custom_code 是選填、4-8 個英數字元,不填會自動產生 6 碼隨機短碼。自訂短碼是付費功能,Free 方案會收到 403og_title / og_description 也是選填,但連結要丟 LINE / Facebook 強烈建議填——預覽卡片好不好看,直接影響點擊率title 是後台辨識用的內部標籤,不會對外顯示TouiError 帶 status 跟 message,分流處理比硬接 generic Error 漂亮很多50 條商品?把 products 換成從 Excel / DB 讀進來的資料,邏輯一樣,兩分鐘跑完。
建完想 spot-check 一下某條連結:
const info = await toui.get("airpods");
console.log("Short code:", info.short_code);
console.log("Target:", info.target_url);
console.log("Title:", info.title);
console.log("Clicks:", info.click_count);
console.log("OG title:", info.og_title);
console.log("Active:", info.is_active); // 真 boolean,SDK 已從 0/1 normalize
回傳欄位(TypeScript 的話 hover 會看到完整型別):
| 欄位 | 說明 |
|---|---|
short_code |
短碼 |
target_url |
目標網址 |
title |
後台用標題 |
click_count |
累計點擊數 |
is_active |
是否啟用中(boolean) |
og_title / og_description / og_image_url |
社群預覽 |
created_at |
建立時間(YYYY-MM-DD HH:MM:SS UTC) |
正式環境通常不會每條都查,但寫進建立流程尾端做一次 spot check,debug 的時候會感謝自己。
週末結束,行銷部門問:「哪個商品點最多?流量從哪來?」
async function report() {
const codes = ["airpods", "airfryer", "vacuum"];
for (const code of codes) {
const stats = await toui.stats(code, { days: 7 });
console.log(`\n--- ${stats.short_code} ---`);
console.log(`Total clicks: ${stats.total_clicks}`);
for (const day of stats.daily) {
console.log(
` ${day.date}: ${day.clicks} (${day.unique_visitors} unique)`,
);
}
if (stats.countries.length) {
console.log(
"Top countries:",
stats.countries.map((c) => `${c.country}:${c.clicks}`).join(", "),
);
}
if (stats.referers.length) {
console.log(
"Top referers:",
stats.referers.map((r) => `${r.referer}:${r.clicks}`).join(", "),
);
}
if (stats.devices.length) {
console.log(
"Devices:",
stats.devices.map((d) => `${d.device}:${d.clicks}`).join(", "),
);
}
}
}
report();
跑出來:
--- airpods ---
Total clicks: 1847
2026-05-03: 823 (614 unique)
2026-05-04: 1024 (789 unique)
Top countries: TW:1650, HK:102, US:53
Top referers: line.me:894, facebook.com:612, direct:341
Devices: mobile:1423, desktop:424
一眼看出:LINE 帶來的流量比 Facebook 多、絕大多數手機點。下次活動素材怎麼調,數據說了算。
注意:進階分析(countries / referers / devices / browsers 的 breakdown)需要 Pro 以上方案。Free 方案的 stats 回傳會帶
limited: true,那幾個 array 會是空的。
| 方案 | 每分鐘 | 每月 API 總量 |
|---|---|---|
| Free | 20 req/min | 5,000 |
| Pro | 200 req/min | 500,000 |
| Business | 600 req/min | 500,000 |
超過會 429。SDK 拋 TouiError 帶 status: 429。一次建幾百條的話,加個簡單 backoff:
async function shortenWithRetry(input, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await toui.shorten(input);
} catch (err) {
if (err instanceof TouiError && err.status === 429) {
const wait = 5 * attempt;
console.log(`Rate limited, waiting ${wait}s (attempt ${attempt})`);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
throw err;
}
}
throw new Error("Max retries exceeded");
}
| HTTP | 意思 | 怎麼處理 |
|---|---|---|
400 |
參數錯誤 | 檢查 request body |
401 |
API Key 無效 | 確認 Authorization header(SDK 自動帶) |
403 |
權限不足(Free 用自訂短碼、目標網址被 Safe Browsing 擋) | 確認方案或目標網址 |
404 |
短碼不存在或不屬於你的團隊 | 確認短碼正確 |
429 |
超速 | backoff 後重試 |
SDK 全部統一拋 TouiError,err instanceof TouiError 一條就分流完。
SDK 接受 fetch 注入,做 logging 或代理很方便:
const toui = new Toui({
apiKey: process.env.TOUI_API_KEY,
fetch: async (input, init) => {
const start = Date.now();
const res = await fetch(input, init);
console.log(
`${init?.method ?? "GET"} ${input} -> ${res.status} (${Date.now() - start}ms)`,
);
return res;
},
});
import { Toui } from "toui-js";
export default {
async fetch(req, env) {
const toui = new Toui({ apiKey: env.TOUI_API_KEY });
const { url } = await req.json();
const link = await toui.shorten({ url });
return Response.json(link);
},
};
Next.js Route Handler、Bun、Deno 同理,只要 runtime 有 fetch 就能跑。
回頭看,你用不到 100 行:
這套包成 script,下次行銷部門丟連結來,改個 products 陣列、跑一次就搞定。
更進階——串電商後台、新品自動建短網址、定期報表寄 email——都是這個基礎再加工。
/admin/api-keys)