
支払い.com事業本部でモバイルアプリエンジニアをしているTominagaです。
なぜCloudflare Workersを選定したのか
技術選定の背景
私たちのチームではCloudflareとGoogle Cloudを中心に技術スタックを構築しています。URL短縮サービスの実装にあたり、以下の理由からCloudflare Workersを選定しました。
既存の技術スタックとの親和性:
- すでにCloudflareをCDNやDNS管理で活用しており、チーム内に運用知見が蓄積されていた
- 追加のインフラ導入なしで既存のCloudflare環境内で完結できる
Honoフレームワークへの技術的関心:
- Honoは軽量で高速なWebフレームワークで、Cloudflare Workersを含む様々なJSランタイムをサポート
- Express.jsライクな書き方でエッジコンピューティングを実装できる
- 軽量かつ高速で、Workers の制約(1MBのコードサイズ制限など)にも適している
- TypeScriptファーストな設計で、型安全な開発が可能
Cloudflare Workersの技術的メリット:
- 🚀 世界275都市でレスポンス50ms以下 - エッジコンピューティングの威力
- 💰 月間10万リクエストまで無料 - スモールスタートに最適
- 🛡️ DDoS攻撃対策込み - Cloudflareの保護機能を自動で利用
- ⚡ コールドスタート0ms - サーバーレス環境での常時高速レスポンス
- 📊 KVストアで簡単にデータ永続化 - DBサーバー不要
実際に社内ツールとして運用してみた結果、選定は正解だったと感じています。特にHonoの開発体験の良さと、Cloudflareのグローバルなインフラを活用できる点が大きなメリットとなりました。
Honoを採用した具体的な理由
Honoは本プロジェクトで以下の点で価値を発揮しました:
優れた開発体験:
- Zodとの統合により、リクエストバリデーションが簡潔に記述できる(
zValidatorミドルウェア) - Express.jsライクなAPIで学習コストが低く、チームメンバーがすぐに開発に参加できた
- TypeScriptの型推論が優秀で、環境変数やコンテキストの型が自動的に解決される
パフォーマンス面の恩恵:
- 軽量設計(約45KB)でWorkers の1MB制限に余裕を持って収まる
- ルーティングエンジンが高速で、複数エンドポイントでも低レイテンシを維持
- 不要な機能がないため、バンドルサイズを最小限に抑えられる
実装の柔軟性:
- ミドルウェアの組み合わせでCORS、認証、ロギングなどを簡単に追加できた
- カスタムコンテキストを定義して、KVへのアクセスを型安全に実装できた
パフォーマンス特性
📊 理論値と実測値:
- P50レスポンスタイム: 20-30ms
- P99レスポンスタイム: 50ms以下
- 同時接続数: 無制限(Cloudflareが自動スケール)
- 月間コスト: $0(10万リクエスト以下)
他アーキテクチャとの比較
| アーキテクチャ | レスポンス時間 | 月額費用(10万req) | グローバル配信 | 運用負荷 |
|---|---|---|---|---|
| Workers + KV | 20-30ms | $0 | ✅ 275都市 | 低 |
| AWS Lambda + DynamoDB | 100-300ms | $5-10 | ❌ リージョン単位 | 中 |
| Cloud Run Functions + Firestore | 100-200ms | $5-8 | ❌ リージョン単位 | 中 |
| Cloud Run + Spanner | 50-100ms | $20+ | ✅ マルチリージョン | 中 |
| EC2 + Redis | 10-50ms | $30+ | ❌ 単一リージョン | 高 |
| Vercel Edge | 50ms | $20 | ✅ 23都市 | 低 |
API機能とエンドポイント一覧
本サービスは以下の機能を提供します:
エンドポイント概要
| エンドポイント | メソッド | 説明 |
|---|---|---|
/:key |
GET | 短縮URLのリダイレクト |
/api/shorten |
POST | URL短縮キーの作成 |
/api/shorten |
GET | URL一覧の取得(ページネーション対応) |
/api/shorten/:key |
GET | 特定URLの詳細取得 |
/api/shorten/:key |
DELETE | URL短縮キーの削除 |
技術スタック
主要技術
- Cloudflare Workers: エッジで動作するサーバーレス実行環境
- Hono: 高速で軽量な Web フレームワーク (v4.6.15)
- TypeScript: 型安全な開発環境
- Cloudflare KV: キーバリューストア(URL マッピングとログ保存用)
開発ツール
- Wrangler: Cloudflare Workers の CLI ツール
- Zod: スキーマバリデーション
- Jest + Miniflare: ローカルテスト環境
- ESLint + Prettier: コード品質管理
アーキテクチャ
システム構成

データモデル
URL_SHORTENER (メインストア)
{ key: string, // 短縮キー (例: "98w2aeok") value: { url: string, // リダイレクト先 URL expireAt?: string, // 有効期限 (ISO8601) createdBy: string, // 作成者メールアドレス createdAt: string // 作成日時 (ISO8601) } }
URL_SHORTENER_LOGS (ログストア)
{ key: string, // 短縮キーと同じ value: { pv: number, // ページビュー数 lastAccessedAt: Date // 最終アクセス日時 } }
社内での活用事例
採用要件と選定理由
URL短縮サービスの導入にあたり、以下の要件を設定しました:
必須要件:
- 社内で使用している技術スタックとの親和性が高いこと
- 低レイテンシ(50ms以下)でグローバルにサービスを提供できること
- 月間10万リクエスト程度の規模で運用コストが低いこと
- 有効期限設定とアクセスログ機能を備えること
パフォーマンス要件:
- P99レスポンスタイムが50ms以下
- 同時アクセスに対するスケーラビリティ
- コールドスタートによる遅延がないこと
運用要件:
- サーバー管理が不要であること
- デプロイが簡単で、CI/CDに組み込みやすいこと
- チーム内に既存の運用知見があること
Cloudflare Workers + Honoの組み合わせは、これらすべての要件を満たし、特にパフォーマンス面では期待を上回る結果となりました。既存のCloudflare環境内で完結できるため、新たなインフラ学習コストも発生しませんでした。
実際の用途
- Slack連携: 長いドキュメントURLを自動短縮してチャンネルに投稿
- QRコード連携: 社内イベントでの資料配布
- 期限付きリンク: 一時的な共有用途(採用面接資料など)
- アクセス解析: どの資料がよく参照されているかの可視化
運用実績
- 平均短縮率: 85%(100文字以上のURLを15文字に)
- 月間生成URL数: 500〜1,000件
- アクティブな短縮URL数: 約3,000件
- 最も使われる機能: 有効期限設定(全体の60%)
主要機能
1. URL 短縮キー生成
export const generateKey = (charCount = 8): string => { const str = Math.random().toString(36).substring(2).slice(-charCount) return str.length < charCount ? str + 'a'.repeat(charCount - str.length) : str }
特徴:
- Base36 (0-9, a-z) でランダムな 8 文字のキーを生成
- 衝突時は自動的に桁数を増やして再生成
- パディング処理で文字数を保証
2. リダイレクト処理
エンドポイント: GET /:key
app.get('*', async (c) => { const url = new URL(c.req.url) const key = url.pathname.slice(1) const json = await c.env.URL_SHORTENER.get(key) if (json == null) { return c.redirect(c.env.NOT_FOUND_REDIRECT_URL, 302) } const { expireAt, url: redirectUrl } = JSON.parse(json ?? '') as { expireAt?: string url: string } // 有効期限チェック if (!redirectUrl || (expireAt && new Date(expireAt) < new Date())) { return c.redirect(c.env.NOT_FOUND_REDIRECT_URL, 302) } // アクセスログ更新 const log = await getLog({ kv: c.env.URL_SHORTENER_LOGS, key }) await updateLog({ kv: c.env.URL_SHORTENER_LOGS, key, log: { pv: log.pv + 1, lastAccessedAt: new Date(), }, }) return c.redirect(redirectUrl, 302) })
処理フロー:
- URL から短縮キーを抽出
- KV から対応する URL を取得
- 存在チェック・有効期限チェック
- アクセスログを更新(PV カウント)
- 302 リダイレクトを返却
3. URL 登録 API
エンドポイント: POST /api/shorten
curl -X POST -H "Content-Type: application/json" \\
-d '{"url":"<https://example.com>", "expireAt": "2025-05-04T21:35:20.333Z", "email": "example@upsider.com"}' \\
<https://your-worker.workers.dev/api/shorten>
リクエストスキーマ (Zod):
z.object({ url: z.string().url(), // 必須: 有効な URL expireAt: z.optional(z.string().datetime()), // 任意: ISO8601 形式 email: z.string().email(), // 必須: 作成者メール })
レスポンス:
{ "key": "98w2aeok" }
4. URL 一覧取得 API
エンドポイント: GET /api/shorten?limit=100&cursor=xxx&keyword=yyy
app.get( '/api/shorten', zValidator( 'query', z.object({ limit: z.string().pipe(z.coerce.number().int().min(0)), cursor: z.optional(z.string()), keyword: z.optional(z.string()), }) ), async (c) => { const { limit, cursor, keyword } = c.req.valid('query') const keys = await c.env.URL_SHORTENER.list({ cursor, limit, prefix: keyword, // 前方一致検索 }) // 詳細情報とログを取得 const logs = await Promise.all( keys.keys.map(async (key) => { const json = await c.env.URL_SHORTENER.get(key.name) const detail = JSON.parse(json ?? '') return { key: key.name, ...detail, } }) ) // createdAt の降順でソート logs.sort((a, b) => { return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() }) return c.json({ hasNext: !keys.list_complete, cursor: keys.list_complete ? undefined : keys.cursor, items: logs, }) } )
レスポンス例:
{ "hasNext": false, "cursor": undefined, "items": [ { "key": "98w2aeok", "url": "<https://example.com>", "expireAt": "2025-05-04T21:35:20.333Z", "createdBy": "example@upsider.com", "createdAt": "2025-01-21T01:29:19.900Z", "pv": 42, "lastAccessedAt": "2025-01-21T01:30:28.224Z" } ] }
5. URL 詳細取得 API
エンドポイント: GET /api/shorten/:key
app.get( '/api/shorten/:key', zValidator('param', z.object({ key: z.string() })), async (c) => { const key = c.req.valid('param').key const json = await c.env.URL_SHORTENER.get(key) if (json == null) { return c.json({ key }) } const detail = JSON.parse(json ?? '') const log = await getLog({ kv: c.env.URL_SHORTENER_LOGS, key: key, }) return c.json({ key, ...detail, ...log }) } )
6. URL 削除 API
エンドポイント: DELETE /api/shorten/:key
curl -X DELETE <https://your-worker.workers.dev/api/shorten/98w2aeok>
すぐに使える実装パターン
バッチでの一括登録
// 複数URLを一度に登録する実装例 async function bulkShorten(urls: string[], email: string) { const results = await Promise.all( urls.map(async (url) => { const key = generateKey() await c.env.URL_SHORTENER.put(key, JSON.stringify({ url, createdBy: email, createdAt: new Date().toISOString() })) return { original: url, short: `${BASE_URL}/${key}` } }) ) return results }
Slack Webhook連携
// 短縮URL作成時にSlackに通知 async function notifySlack(key: string, url: string, createdBy: string) { await fetch(SLACK_WEBHOOK_URL, { method: 'POST', body: JSON.stringify({ text: `新しい短縮URLが作成されました`, blocks: [{ type: 'section', text: { type: 'mrkdwn', text: `*作成者:* ${createdBy}\\n*短縮URL:* ${BASE_URL}/${key}\\n*元URL:* ${url}` } }] }) }) }
実装でハマったポイントと解決策
1. KVの結果整合性問題
Cloudflare KVは結果整合性のため、書き込み直後の読み取りで古いデータが返ることがあります。
問題のシナリオ:
// NG: 書き込み直後に読み取ると、まだ反映されていない可能性 await c.env.URL_SHORTENER.put(key, value) const result = await c.env.URL_SHORTENER.get(key) // null or 古い値
解決策:
// OK: 書き込み後のレスポンスには書き込んだ値を使う const data = { url, expireAt, createdBy, createdAt } await c.env.URL_SHORTENER.put(key, JSON.stringify(data)) return c.json({ key, ...data }) // KVから読まずに返す
2. 開発環境でのKVエミュレーション
Miniflareバージョンによる挙動の違いに注意が必要です。
解決策:
# Wrangler 3.x以降の--localフラグで安定動作 npm run dev -- --local
3. 型安全性の確保
KVから取得したJSONの型が保証されない問題。
解決策: Zodでランタイムバリデーション
const kvDataSchema = z.object({ url: z.string().url(), expireAt: z.string().datetime().optional(), createdBy: z.string().email(), createdAt: z.string().datetime() }) const data = kvDataSchema.parse(JSON.parse(kvValue))
4. KVNamespaceの型が効かない問題
Cloudflare WorkersでKVNamespaceを使用する際、TypeScriptの型推論が効かず、エディタで赤線が表示されたり、ビルドエラーになることがあります。
よくある症状:
// ❌ エラー: Cannot find name 'KVNamespace' type Bindings = { URL_SHORTENER: KVNamespace } // ❌ エラー: Property 'URL_SHORTENER' does not exist on type 'Env' const value = await c.env.URL_SHORTENER.get(key)
原因と解決策:
解決策1: @cloudflare/workers-typesの正しい設定
// 1. パッケージをインストール npm install -D @cloudflare/workers-types // 2. tsconfig.jsonに追加 { "compilerOptions": { "types": ["@cloudflare/workers-types"] } } // 3. 型定義ファイル(types.d.ts)を作成 /// <reference types="@cloudflare/workers-types" /> declare module "*.html" { const content: string export default content }
解決策2: Honoの型定義を正しく設定
// src/types.ts - 型定義を別ファイルに分離 export type Bindings = { URL_SHORTENER: KVNamespace URL_SHORTENER_LOGS: KVNamespace NOT_FOUND_REDIRECT_URL: string ROOT_REDIRECT_URL: string ALLOWED_ORIGINS: string } // src/index.ts - メインファイル import { Hono } from 'hono' import type { Bindings } from './types' const app = new Hono<{ Bindings: Bindings }>() // これで c.env の型が正しく推論される app.get('/:key', async (c) => { // ✅ 型が効く const value = await c.env.URL_SHORTENER.get(c.req.param('key')) })
解決策3: wrangler.tomlとの型同期
// worker-configuration.d.ts interface Env { // wrangler.tomlのbinding名と完全一致させる URL_SHORTENER: KVNamespace URL_SHORTENER_LOGS: KVNamespace // 環境変数も型定義 NOT_FOUND_REDIRECT_URL: string ROOT_REDIRECT_URL: string ALLOWED_ORIGINS: string }
解決策4: VSCode設定の調整
// .vscode/settings.json { "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, "typescript.preferences.includePackageJsonAutoImports": "on" }
5. ローカル開発時のKVNamespace型エラー
Miniflareでローカル開発する際、型定義が異なることがあります。
問題のコード:
// ローカルでは動くが、デプロイすると型エラー const kv = c.env.URL_SHORTENER as unknown as KVNamespace
推奨される解決策:
// 環境に依存しない型安全な実装 import type { KVNamespace } from '@cloudflare/workers-types' // ジェネリック型で抽象化 type KVStore = { get: KVNamespace['get'] put: KVNamespace['put'] delete: KVNamespace['delete'] list: KVNamespace['list'] } // ヘルパー関数で型を保証 async function getFromKV( kv: KVNamespace, key: string ): Promise<string | null> { try { return await kv.get(key) } catch (error) { console.error('KV get error:', error) return null } } // 使用例 app.get('/:key', async (c) => { const value = await getFromKV(c.env.URL_SHORTENER, c.req.param('key')) if (!value) { return c.notFound() } // ... })
6. テスト環境でのKVNamespaceモック
// test/helpers/kv-mock.ts export class KVNamespaceMock implements KVNamespace { private store = new Map<string, string>() async get(key: string): Promise<string | null> { return this.store.get(key) || null } async put(key: string, value: string): Promise<void> { this.store.set(key, value) } async delete(key: string): Promise<void> { this.store.delete(key) } async list(options?: KVNamespaceListOptions): Promise<KVNamespaceListResult> { const keys = Array.from(this.store.keys()) return { keys: keys.map(name => ({ name, metadata: null })), list_complete: true, cursor: '' } } // 他の必要なメソッドも実装 getWithMetadata(): Promise<KVNamespaceGetWithMetadataResult> { throw new Error('Not implemented') } } // test/index.test.ts import { KVNamespaceMock } from './helpers/kv-mock' describe('URL Shortener', () => { let env: Bindings beforeEach(() => { env = { URL_SHORTENER: new KVNamespaceMock() as unknown as KVNamespace, URL_SHORTENER_LOGS: new KVNamespaceMock() as unknown as KVNamespace, // ... } }) test('should store and retrieve URL', async () => { await env.URL_SHORTENER.put('test', JSON.stringify({ url: '<https://example.com>' })) const result = await env.URL_SHORTENER.get('test') expect(result).toBeTruthy() }) })
トラブルシューティングチェックリスト
KVNamespaceの型エラーが発生した場合のチェックリスト:
- ✅
@cloudflare/workers-typesが最新バージョンか確認 - ✅
tsconfig.jsonに types 設定があるか確認 - ✅
wrangler.tomlのbinding名と型定義が一致しているか確認 - ✅ Honoのバージョンが4.0以上か確認(型サポートが改善)
- ✅ VSCodeを再起動して型キャッシュをクリア
- ✅
node_modulesとpackage-lock.jsonを削除して再インストール
環境構築
1. 前提条件
- Node.js 18 以上
- npm または yarn
- Cloudflare アカウント
- Wrangler CLI
2. インストール
# 依存パッケージインストール npm install
3. 環境変数設定
.dev.vars ファイルを作成:
ROOT_REDIRECT_URL="<http://localhost:1234/ROOT>" NOT_FOUND_REDIRECT_URL="<http://localhost:1234/NOT_FOUND>" ALLOWED_ORIGINS="<http://localhost:3001>,<http://localhost:3002>"
4. KV Namespace 作成
# 本番環境用 wrangler kv:namespace create "URL_SHORTENER" --env production wrangler kv:namespace create "URL_SHORTENER_LOGS" --env production # ステージング環境用 wrangler kv:namespace create "URL_SHORTENER" --env staging wrangler kv:namespace create "URL_SHORTENER_LOGS" --env staging
生成された ID を wrangler.toml に設定:
kv_namespaces = [ { binding = "URL_SHORTENER", id = "<YOUR_KV_ID>" }, { binding = "URL_SHORTENER_LOGS", id = "<YOUR_LOG_KV_ID>" } ]
5. ローカル開発
# 開発サーバー起動 npm run dev # <http://localhost:8787> でアクセス可能
6. デプロイ
# ステージング環境 npm run deploy:stg # 本番環境 npm run deploy:prod
参考: 実際に動作する最小構成
// package.json の dependencies { "dependencies": { "hono": "^4.6.15" }, "devDependencies": { "@cloudflare/workers-types": "^4.20241004.0", "wrangler": "^3.80.0", "typescript": "^5.5.4" } } // tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "lib": ["ES2022"], "types": ["@cloudflare/workers-types"], "moduleResolution": "bundler", "strict": true, "skipLibCheck": true } } // src/index.ts import { Hono } from 'hono' type Bindings = { URL_SHORTENER: KVNamespace } const app = new Hono<{ Bindings: Bindings }>() app.get('/test', async (c) => { // 型が正しく推論される const value = await c.env.URL_SHORTENER.get('test') return c.json({ value }) }) export default app
本番運用のセキュリティ対策
1. 内部利用限定の実装
// IPアドレス制限の実装例 const allowedIPs = ['192.168.1.0/24', '10.0.0.0/8'] app.use('*', async (c, next) => { const ip = c.req.header('CF-Connecting-IP') if (!isAllowedIP(ip, allowedIPs)) { return c.text('Forbidden', 403) } await next() })
2. レート制限の実装
// KVを使った簡易レート制限 async function checkRateLimit(email: string): Promise<boolean> { const key = `ratelimit:${email}:${new Date().toISOString().slice(0, 10)}` const count = await c.env.URL_SHORTENER.get(key) if (parseInt(count || '0') > 100) { return false // 1日100件まで } await c.env.URL_SHORTENER.put(key, String(parseInt(count || '0') + 1), { expirationTtl: 86400 // 24時間後に自動削除 }) return true }
3. 不正URL対策
// 社内ドメインのみ許可 const allowedDomains = ['example.com', 'internal.example.com'] function validateUrl(url: string): boolean { const parsed = new URL(url) return allowedDomains.some(domain => parsed.hostname.endsWith(domain) ) }
CORS 設定
API エンドポイント (/api/*) には CORS ミドルウェアを適用:
const corsMiddleware = async ( context: Context<{ Bindings: Bindings }, '*', {}>, next: () => Promise<void> ) => { const arrowedOrigins: string[] | string = context.env.ALLOWED_ORIGINS.includes(',') ? context.env.ALLOWED_ORIGINS.split(',') : context.env.ALLOWED_ORIGINS const corsMiddleware = cors({ origin: arrowedOrigins, allowHeaders: [], allowMethods: ['POST', 'GET', 'DELETE'], exposeHeaders: ['Content-Length'], maxAge: 600, credentials: false, }) return await corsMiddleware(context, next) } app.use('/api/*', corsMiddleware)
環境別設定:
- Production: 特定のオリジンのみ許可
- Staging:
ALLOWED_ORIGINS="*"でワイルドカード許可
よくあるトラブルと対処法
Q: KVの書き込み上限に達した
A: KVは1秒あたり1,000書き込みが上限。バッチ処理には遅延を入れる:
for (const batch of chunks(items, 100)) { await Promise.all(batch.map(item => processItem(item))) await new Promise(resolve => setTimeout(resolve, 1000)) }
Q: 短縮キーが重複してエラーになる
A: 重複チェックとリトライ処理を実装:
const MAX_RETRIES = 5 for (let i = 0; i < MAX_RETRIES; i++) { const key = generateKey(8 + i) // 衝突したら文字数を増やす const existing = await c.env.URL_SHORTENER.get(key) if (!existing) return key } throw new Error('Failed to generate unique key')
Q: ログデータが大きくなりすぎる
A: 定期的な集計と古いログの削除:
// 30日以上前のログを削除するCron Trigger export async function scheduled(event: ScheduledEvent, env: Env) { const thirtyDaysAgo = new Date() thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30) const keys = await env.URL_SHORTENER_LOGS.list() for (const key of keys.keys) { const log = await env.URL_SHORTENER_LOGS.get(key.name) const { lastAccessedAt } = JSON.parse(log) if (new Date(lastAccessedAt) < thirtyDaysAgo) { await env.URL_SHORTENER_LOGS.delete(key.name) } } }
テスト
Jest + Miniflare でのローカルテスト
// jest.config.js module.exports = { testEnvironment: 'miniflare', testMatch: [ '**/test/**/*.+(ts|tsx|js)', '**/src/**/(*.)+(spec|test).+(ts|tsx|js)', ], transform: { '^.+\\\\.(ts|tsx)$': 'esbuild-jest', }, }
npm run test
Miniflare により、ローカル環境で Workers と KV の動作を完全に再現可能。
監視とログ
Workers Logs
wrangler.toml で有効化:
[observability] enabled = true
アクセスログ
- PV カウント
- 最終アクセス日時
- KV に永続化されたログデータ
Cloudflare ダッシュボードでリアルタイム監視が可能。
実装のポイント
1. キー衝突回避
let keyLength = 8 let key = generateKey(keyLength) while (await c.env.URL_SHORTENER.get(key)) { keyLength++ // 衝突したら桁数を増やす key = generateKey(keyLength) }
2. エラーハンドリング
export const getLog = async ({ kv, key }): Promise<LogType> => { try { const json = await kv.get(key) const log = JSON.parse(json ?? '') as LogType return log } catch (e) { // 初回アクセス時はデフォルト値を返す return { pv: 0, lastAccessedAt: new Date(), } } }
3. 型安全性
type Bindings = { URL_SHORTENER: KVNamespace URL_SHORTENER_LOGS: KVNamespace NOT_FOUND_REDIRECT_URL: string ROOT_REDIRECT_URL: string ALLOWED_ORIGINS: string } const app = new Hono<{ Bindings: Bindings }>()
Hono の型システムを活用し、環境変数への型安全なアクセスを実現。
今後の拡張案
1. カスタムエイリアス機能
ユーザーが任意のキーを指定できる機能:
POST /api/shorten { "url": "<https://example.com>", "customKey": "my-link" // オプション }
2. QR コード生成
短縮 URL の QR コード画像を自動生成:
GET /api/shorten/:key/qrcode
3. アクセス解析
- 国別アクセス統計
- リファラー情報
- デバイス種別
Cloudflare Analytics Engine との連携で実現可能。
4. レート制限
DoS 攻撃対策として、IP ベースのレート制限を追加:
import { rateLimiter } from 'hono-rate-limiter' app.use('/api/*', rateLimiter({ windowMs: 15 * 60 * 1000, max: 100 }))
5. バルク登録 API
複数 URL の一括登録機能:
POST /api/shorten/bulk { "urls": [ { "url": "<https://example1.com>", "email": "user@example.com" }, { "url": "<https://example2.com>", "email": "user@example.com" } ] }
まとめ:導入判断のポイント
✅ Cloudflare Workers + KVが適している場合
- 社内ツールやスモールスタートのサービス
- グローバルに低遅延でサービスを提供したい
- サーバー管理のコストを削減したい
- 月間100万リクエスト以下の規模
- すでにCloudflareを利用している組織
⚠️ 他の選択肢を検討すべき場合
- 複雑なリレーショナルデータを扱う必要がある
- 1秒1,000件以上の書き込みが常時発生する
- 25MBを超える大きなデータを保存する必要がある
- WebSocketなどのリアルタイム通信が必須
実装にかかる工数目安
- 基本機能の実装: 1-2日
- アクセスログ機能: 0.5日
- 管理画面の作成: 2-3日
- 本番環境構築: 0.5日
- 合計: 約1週間で本番運用可能
参考リンク
We Are Hiring !!
UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。
UPSIDER Engineering Deckはこちら📣