Cloudflare PagesでURL短縮サービスをつくる!
Cloudflare PagesでURL短縮サービスを作ってみましょう!これを作ることであなたは以下を体験することができるしょう。
- HonoでWebページをつくること
- Cloudflare KVをアプリケーションの中で使うこと
- アプリケーションをCloudflare Pagesへデプロイすること
アプリケーションの特徴
今回作ってもらうアプリケーションはこのような特徴があります。
- Viteを使って開発
- UI付き
- JSXを使ってHTMLを書ける
- メインのコードは100行以下!
- Zodを使ったバリデーション
- バリデーションエラーも表示
- 簡易なCSRF対策
デモ
完成品を使っている様子です。
完成品
完成済みのコードは以下にあります。
アカウント
今回、アプリケーションを作ってCloudflare PagesへデプロイするにはCloudflareのアカウントが必要です。無料の範囲で遊べるので、もってない人はアカウトを作っておいてください。
プロジェクトのセットアップ
では作っていきます。まず、プロジェクトをセットアップします。
初期プロジェクト
"create-hono"というCLIを使ってプロジェクトを作ります。以下のコマンドを実行します。
npm create hono@latest url-shortener
すると、どのテンプレートにするか選択肢が現れるので"cloudflare-pages"を選びます。その後、依存関係をインストールするか、どのパッケージマネージャーを使うかを聞かれるのでどちらもEnterを押して次に進みます。
これで初期プロジェクトができるので、その中に入っておきましょう。
cd url-shortener
開発サーバーを立ち上げる
開発サーバーを立ち上げてみましょう。といっても簡単です。以下のコマンド実行するだけです。
npm run dev
するとデフォルトでは「http://localhost:5173
」で立ち上がるのでそこにアクセスします。ページが見れたでしょう。
KVを作る
このアプリではCloudflare KVというKey-Valueストアを使います。使うためにはKVのプロジェクトを作らなくてはいけないので以下のコマンドを実行します。
npm exec wrangler kv:namespace create KV
すると以下のようなメッセージが表示されます。
🌀 Creating namespace with title "url-shortener-KV"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "KV", id = "xxxxxx" }
このid
のxxxxxx
部分をコピペしておいて、それを以下のフォーマットでwrangler.toml
に書き込みます。
[[kv_namespaces]]
binding = "KV"
id = "xxxxxx"
これでKVの設定は終わりです。
依存ライブラリをインストールする
今回のアプリでは入力値の検証をしっかりやります。そのためにZodというライブラリとHonoのミドルウェアを入れます。
npm i zod @hono/zod-validator
public
を消しておく
最後に、スターターテンプレートには自分で書く用のCSSが入っているpublic
があるのですが、今回は使わないので消しておきます。
rm -rf public
コードを書く
ではコードを書いていきましょう。
レイアウトを整える
ページ共通で使う「ガワ」を整えます。そのためにはsrc/renderer.tsx
を編集します。
手間はかけたくないので、new.cssというCSSフレームワークを使います。これはClass-lessフレームワークといって、特にclass
属性に特別な値を入れずとも、これまでのHTMLそのままでいい感じのスタイルを当ててくれるというものです。
最終的にこのようになりました。
import { jsxRenderer } from 'hono/jsx-renderer'
export const renderer = jsxRenderer(({ children }) => {
return (
<html>
<head>
<link rel="stylesheet" href="https://fonts.xz.style/serve/inter.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css"></link>
</head>
<body>
<header>
<h1>
<a href="/">URL Shortener</a>
</h1>
</header>
<div>{children}</div>
</body>
</html>
)
})
トップページを作る
最初につくるページはトップページです。GETリクエストが/
というパスに来た場合に応答します。ルーティングはこのようになります。
app.get('/', (c) => {
//...
})
ハンドラの中ではc.render()
というメソッドの中にJSXを渡せば、レイアウトを適用したHTMLのレスポンスを返します。/create
というパスにPOSTリクエストを送って短縮URLをつくるようにしました。
app.get('/', (c) => {
return c.render(
<div>
<h2>Create shorten URL!</h2>
<form action="/create" method="post">
<input
type="text"
name="url"
autocomplete="off"
style={{
width: '80%'
}}
/>
<button type="submit">Create</button>
</form>
</div>
)
})
すると以下のような見た目になるでしょう。
バリデータを作る
トップページからのフォームデータを受け取る際に値の検証をしたいのでそのためのバリデータを作りましょう。
先ほどインストールしたライブラリからオブジェクトをimport
します。
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
次にスキーマを作ります。「url
という名前でURL形式の文字列を受け取る」ということを以下のように書くことができます。
const schema = z.object({
url: z.string().url()
})
それをzValidator
に登録します。第一引数に渡しているform
というのはフォームリクエストをハンドリングしたいから指定しています。
const validator = zValidator('form', schema)
/create
に来たPOSTリクエストを処理するエンドポイントをつくって、完成したバリデータを使ってみましょう。バリデータはミドルウェアなので、このようにハンドラの前に挟むことができます。そして、c.req.valid()
メソッドで値を取得できます。この場合はurl
という名前です。
app.post('/create', validator, async (c) => {
const { url } = c.req.valid('form')
// TODO: Create a short URL
})
検証を通過したらその値がurl
に入るでしょう。
KVの型定義をする
フォームからの値が取れたので、次に短縮URLをつくるロジックを書きます。
その前にアプリケーションの中で使うKVの型定義をしておきます。KVNamespace
という型がKVを表します。Honoの場合、Bindings
という名前でCloudflareのBindingsの型をHonoクラスのジェネリクスに渡すとその後、c.env.KV
のように型付きでアクセスできるようになります。
type Bindings = {
KV:KVNamespace
}
const app = new Hono<{
Bindings: Bindings
}>()
キーを生成、保存する
短縮URLのパスに当たるキーを生成する関数をcreateKey()
という名前で作りましょう。キーの生成にはKVオブジェクトとURLが必要になります。
app.post('/create', validator, async (c) => {
const { url } = c.req.valid('form')
const key = await createKey(c.env.KV, url)
// ...
})
キーを生成するロジックはいくつか考えつきますが、今回はこの作戦でいきます。
- ランダムな文字列を生成する
- そのうちの6文字を利用する
- KVの中にそれをキーとしたオブジェクトがなければURLを値として保存する
- KVの中にそれをキーとしたオブジェクトがあればもう一度
createKey()
を実行する - 作られたキーを返す
KVはkv.get(key)
で値の取得、kv.put(key, value)
で値のセットができます。
完成した関数はこのようになりました。
const createKey = async (kv: KVNamespace, url: string) => {
const uuid = crypto.randomUUID()
const key = uuid.substring(0, 6)
const result = await kv.get(key)
if (!result) {
await kv.put(key, url)
} else {
return await createKey(kv, url)
}
return key
}
結果を表示する
これでキーの作成ができました。キーの値をパス名にしたURLが短縮URLになります。もしローカルで開発してて、例えば、abcdefだったら
http://localhost:5173/abcdef
となります。そのURLをコピーするのに便利なようにinput
要素の中に表示するページをつくりました。autofocus
も使っています。
app.post('/create', validator, async (c) => {
const { url } = c.req.valid('form')
const key = await createKey(c.env.KV, url)
const shortenUrl = new URL(`/${key}`, c.req.url)
return c.render(
<div>
<h2>Created!</h2>
<input
type="text"
value={shortenUrl.toString()}
style={{
width: '80%'
}}
autofocus
/>
</div>
)
})
すると短縮URLが作成され、いい感じに表示されるようになるでしょう。
リダイレクトさせる
短縮URLの生成はできたので、そこにアクセスすると登録されているURLへリダイレクトするようにしましょう。/abcedf
というアドレスにマッチさせるには正規表現でルーティングのパスを指定すればいいです。そして、ハンドラの中ではその文字列をキーにKVから値を取得して、ある場合はそれが短縮前のURLなので、そこへリダイレクトさせます。なければアプリのトップページへリダイレクトです。
app.get('/:key{[0-9a-z]{6}}', async (c) => {
const key = c.req.param('key')
const url = await c.env.KV.get(key)
if (url === null) {
return c.redirect('/')
}
return c.redirect(url)
})
エラー処理をする
ここまで来るとだいたいできました。いい感じです!
しかし、イケてないのはフォームにURLではない値を入れた場合です。バリデータが機能しているので、しっかりとエラーになるのですが、JSONの文字列が表示されるだけです。
エラーページを表示するようにしましょう。そのためにはzValidator
の第3引数にフックを書きます。result
はZodでバリデーションした結果オブジェクトなので、それを使って成功したかどうかを判断しています。
const validator = zValidator('form', schema, (result, c) => {
if (!result.success) {
return c.render(
<div>
<h2>Error!</h2>
<a href="/">Back to top</a>
</div>
)
}
})
これで、バリデーションエラーが起こった場合はエラーが表示されます。
CSRFプロテクターを入れる
これで最後です!今の状態でも十分素晴らしいURL短縮サービスなのですが、異なるサイトのフォームから直接このアプリのフォームにPOSTリクエストがいってしまう場合があります。そこで、HonoのビルドインミドルウェアであるCSRF Protectorを使います。
使い方はとっても簡単。importします。
import { csrf } from 'hono/csrf'
使いたいルートのハンドラの前に挟みます。
app.post('/create', csrf(), validator, async (c) => {
const { url } = c.req.valid('form')
const key = await createKey(c.env.KV, url)
//...
これで完成です!あなたは100行以内でindex.tsx
でUI付き、バリデーション付き、エラー処理付き、CSRF対策付きのURL短縮アプリを作ったのです!
デプロイをする
Cloudflare Pagesへデプロイしてみましょう。以下のコマンドを実行します。
npm run deploy
初めての場合以下のように質問されるので、答えます。
Create a new project
? Enter the name of your new project: › url-shortener
? Enter the production branch name: › main
実行するとデプロイ先のURLが表示されます。おそらく以下のようなものでしょう。
https://random-strings.url-shortener-abc.pages.dev/
作成されてから見ることができるようになるまでしばらく時間がかかるので待ちましょう。場合によっては先頭のホスト名を取り除いたurl-shortener-abc.pages.dev
にアクセスと見れる場合があります。
ダッシュボードでKVの指定をする
しかし!こままでは「Internal Server Error」が出てると思います。これはKVの設定が本番環境でされてないからです。現状ではwrangler.toml
に設定を書いても、それは反映されず、ダッシュボードでの指定が必要です。作成したPagesプロジェクトの設定画面からKVの項目へいき、KVという名前で先程作成したKVの名前空間を指定しましょう。
これで再びデプロイをすると...動いてるはずです!
プロジェクトを消す
使わない人は本番のPagesプロジェクトを消しておきましょう。
まとめ
短縮URLアプリをCloudflare KVとHonoを使って作り、Cloudflare Pagesにデプロイしてみました。メインのsrc/index.tsx
が100行ほどですが、「JSONを返すだけ」でもなくしっかりページ付きでバリデーション、エラー処理ができてる立派なアプリになりました。このままだと外部の人が無限に作成できて、無限にKVを叩けるのでその点など考慮すべきは残っているので、そのあたりは宿題にしてください。
以上、いかがでしたでしょうか。いい感じでしょ?Honoを使ったCloudflare Pagesアプリの作成は可能性があるので、試してみてください。また、より大きなアプリを作る場合はファイルベースルーティングができるHonoXの方が便利な場合があるのでそれも使ってみてください。
最後に完成形のレポジトリを再掲します。
Discussion