Fiber + Redis で URL Shortenerを実装し、仕組みを理解する
はじめに
今回は、Go言語のFiber
フレームワークを使用して、URL Shortenerを実装し、Postman
でAPIテストするところまでをまとめていこうと思います。
このURL Shortener
は、ユーザーが入力したURLを短縮し、その短縮URLを使用して元のURLにリダイレクトできるシステムです。
また、実際のコードを抜粋しながら、各部分の重要なロジックを丁寧に解説していきます。
📌 本記事で解説する内容
- Fiber を使用した URL Shortener API の実装
- Redis を使用した短縮URLと元のURLの保存方法
- レートリミットの設定 で不正アクセスを制御
- Postman を使った API テストでの動作確認
使用技術
-
Fiber
- Go言語のフレームワーク
- Goバージョン:
1.24.2
-
Redis
- URL保存
- レートリミット
-
Postman
- APIテストツール
-
Docker
- Go + Redis の環境構築
本記事では、Dockerでの環境構築は省略しますが、GitHubリポジトリを公開しているので、セットアップする際はそちらをご覧ください。
Redisについて
URL Shortener
では、短縮されたURLと元のURLを即座に紐付け、短縮URLのアクセス時に、瞬時にリダイレクトできることが重要になります。
そのため、高速なデータアクセスが求められます。
Redis は 「インメモリデータベース」 として、すべてのデータをメモリ(RAM)に保存し、高速な読み書きを実現します。
今回、MongoDB
ではなく Redis
を選んだ理由は以下の通りです。
-
シンプルなKey-Value構造が最適
- URL Shortenerでは「短縮URL(Key)」と「元のURL(Value)」を保存するだけのシンプルな構造で十分
-
MongoDB
は複雑なドキュメント構造を扱う用途に向いており、今回の用途にはオーバースペック
-
レートリミットの管理が容易
- IPアドレスごとにリクエスト数をカウントし、レートリミットの管理が容易
- レートリミットカウントとリセットをシンプルに管理可能
Redisの特徴
-
インメモリデータベース
- データはすべて
RAM
上に保存され、ミリ秒単位での読み書きが可能
- データはすべて
-
Key-Value ストア(KVS)
-
Key : Value
の組み合わせでデータを保存
-
-
高いスケーラビリティ
- クラスタにも対応し、大量アクセスにも対応可能
-
多用途
- キャッシュ、セッション管理、レートリミット、ランキングシステムなど様々な用途に利用可能
URL Shortenerの全体の流れ
1. ユーザーがURLを入力し、短縮をリクエスト
- クライアント(Postman)からAPIに、
POST /api/v1/shorten
リクエストを送信 - 送信データには以下が含まれます
-
URL
:短縮したい元のURL -
CustomShort
(任意):カスタムの短縮コード(指定されていないと自動生成) -
Expiry
(任意) :URLの有効期限(デフォルトでは24時間)
-
- Redis(DB 0)に以下を保存
// Redisの保存例
DB 0:
Key: abc123
Value: https://example.com
2. URL検証と短縮URLの生成
- バリデーション
- 入力されたURLが存在する or 正しい形式かをチェック
- ドメインチェック
- 短縮URLが自分のサービスドメインと重複しないかチェック
- 今回は検証なので
DOMAIN
という環境変数にlocalhost:3000
を入れておきます
- 短縮コードの生成
- ユーザーが指定の
CustomShort
を入力(任意) - 何も入力がない場合はUUIDを使用しランダムに生成
- ユーザーが指定の
- レスポンス
- 短縮URLをユーザーに返す
3. ユーザーが短縮URLにアクセス
- クライアントが
GET /:url
(短縮URL)にアクセス - サーバーはRedis(DB 0)から短縮URLに対応する元のURLを検索し、リダイレクト
- アクセスカウンター
- Redis(DB 1)でアクセス回数を記録
Redisのデータ構造をざっくりまとめると
- Key-Value形式でURLを保存
- DB 0:短縮URL(ショートコード)と元のURLのペア
- DB 1:レートリミット(IPアドレスごとのカウント)およびアクセスカウンター
DB 0 (短縮URL - 元のURL)
Key | Value |
---|---|
abc123 | https://example.com |
xyz789 | https://testfuji.com |
custom1 | https://yourdomain.com |
DB 1 (IPアドレスごとのカウント)
Key | Value |
---|---|
192.168.1.1 | 15 (残りリクエスト数) |
counter | 12 (アクセス数) |
ソースコード
ここからは重要な箇所を抜粋して、各部分を解説していこうと思います。
✅ URL短縮 APIエンドポイント(POST /api/v1/shorten)
- ユーザーがURLを短縮するリクエストを送信し、短縮URLを生成して返す
func ShortenURL(c *fiber.Ctx) error {
// JSON パース
body := new(request)
if err := c.BodyParser(body); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "cannot parse JSON"})
}
-
JSONパース
- JSON形式のリクエストボディをパースし、
request
構造体にマッピング
- JSON形式のリクエストボディをパースし、
- リクエストが無効(JSONパースエラー)の場合は、
400 Bad Request
エラーを返す- 空のリクエスト、破損したJSONがそのまま通過しないように制御
✅ レートリミットの設定
- ユーザーのIPアドレスごとにリクエスト数を制御(Redis DB 1)
quotaStr := os.Getenv("API_QUOTA")
if quotaStr == "" {
quotaStr = "15"
}
quota, _ := strconv.Atoi(quotaStr)
resetMin := 30
if v := os.Getenv("RATE_LIMIT_RESET"); v != "" {
if x, err := strconv.Atoi(v); err == nil {
resetMin = x
}
}
resetDur := time.Duration(resetMin) * time.Minute
rlDB := databases.CreateClient(1)
defer rlDB.Close()
// IPごとのレートリミットカウント
countStr, err := rlDB.Get(databases.Ctx, c.IP()).Result()
if err == redis.Nil {
rlDB.Set(databases.Ctx, c.IP(), quota, resetDur)
} else if n, _ := strconv.Atoi(countStr); n <= 0 {
ttl, _ := rlDB.TTL(databases.Ctx, c.IP()).Result()
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": "Rate Limit Exceeded",
"rate_limit_reset": ttl / time.Second,
})
}
- レートリミットの設定
-
API_QUOTA
環境変数でリクエスト上限を設定 - 悪意のあるユーザーに過剰なリクエストを送信させないようにするため
-
- IPアドレスごとのリクエストを
DB 1
で管理- 各ユーザー(IPアドレスごと)に対し、リクエスト回数のカウントが可能
-
API_QUOTA
とRATE_LIMIT_RESET
で動的にリクエスト数とリセット時間を設定可能
✅ URL検証と短縮URLの生成
- ユーザーの送信したURLが有効かをチェックし、短縮URLを生成
// URL検証
if !govalidator.IsURL(body.URL) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid URL"})
}
bodyURL := helpers.EnforceHTTP(body.URL)
if !helpers.RemoveDomainError(bodyURL) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Disallowed domain"})
}
key := body.CustomShort
if key == "" {
key = uuid.New().String()[:6]
}
- URLバリデーション
-
govalidator
でURL形式をチェック -
helpers.EnforceHTTP
で「http://」を強制 -
helpers.RemoveDomainError
で自身のドメイン(localhost:3000)を除外 - これらはhelpersパッケージに関数を作成し、それを呼んでいます
-
- 短縮URLの生成
-
CustomShort
が指定されていればそれを使用 - そうでなければUUIDからランダムな6文字を生成
-
✅ Redis に短縮URLを保存
- 短縮URLと元のURLをRedis(DB 0)に保存
// すでに存在しないか確認
dataDB := databases.CreateClient(0)
defer dataDB.Close()
if ex, _ := dataDB.Exists(databases.Ctx, key).Result(); ex > 0 {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "Short key already used"})
}
// 有効期限
hours := body.ExpiryHours
if hours <= 0 {
hours = 24
}
expDur := time.Duration(hours) * time.Hour
dataDB.Set(databases.Ctx, key, bodyURL, expDur
- 短縮URLの重複チェック
- すでに同じ短縮コードが存在する場合はエラーを返す
- URLを保存
- 短縮コード(Key)と元のURL(Value)を保存
- 有効期限を指定(デフォルトは24時間)で保存
✅ URLリダイレクト (GET /:url)
- 短縮URLにアクセスすると、元のURLにリダイレクトさせる
func ResolveURL(c *fiber.Ctx) error {
ctx := context.Background()
url := c.Params("url")
r := databases.CreateClient(0)
defer r.Close()
val, err := r.Get(databases.Ctx, url).Result()
if err == redis.Nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "short not found in database"})
} else if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "cannot connect to DB"})
}
// アクセスカウンター
rInr := databases.CreateClient(1)
defer rInr.Close()
_, err = rInr.Incr(ctx, "counter").Result()
return c.Redirect(val, fiber.StatusMovedPermanently)
}
- 短縮URLで元のURLを検索
- 元のURLが見つかればリダイレクト(
301
) - 見つからなければ
404 Not Found
を返す
- 元のURLが見つかればリダイレクト(
- アクセスカウンター
-
DB 1
でcounterをインクリメントし、アクセス数を記録
-
PostmanでAPIテスト手順
テストには、私が以前Qiitaに投稿した記事を使用します。
1.URLのみ指定
まずは、URLのみの指定でPOSTリクエストを送っていきます。
Postmanで以下の設定を行い、「Send」をクリックしましょう。
下の画像のJSONが得られ、短縮URLにアクセスできたら成功です。
2.カスタムURLを指定
続いて、CustomShort
を設定し、反映されるかチェックしていきましょう。
先ほどのJSONの部分を以下に変更して、リクエストを送ってみます。
{
"url": "https://qiita.com/fujifuji1414/items/359d754f9ab0ad2ccbb7",
"short": "fuji14"
}
以下の画像のようにCustomShort
が作成せれていれば成功です。
ではurl
部分を変更し、CustomShort
はそのままでエラーがちゃんと出力するか確認していきます。
「"error": "Short key already used"
」が出力されたので、テスト成功です。
3.有効期限を指定
JSONの項目にexpiry
を追加して、リクエストを送っていきます。
expiry
は時間していで、以下の例だと「1時間」を指定していることになります。
{
"url": "https://qiita.com/fujifuji1414/items/359d754f9ab0ad2ccbb7",
"expiry": 1
}
"expiry": 3600000000000
(msで返ってくる)が反映されているので成功です。
4.無効なURLを指定
以下のJSONを指定してみます。
{
"url": "javascript:alert('XSS')"
}
画像の通り、バリデーションできているので成功です。
5.レートリミット超過
最後に、同一IPアドレスから連続でリクエストを送信し、レートリミットが超過した時の挙動をチェックしていきます。
{
"url": "https://qiita.com/fujifuji1414/items/359d754f9ab0ad2ccbb7"
}
こちらのJSONを16回リクエストを送信し、以下を確認できるかテストしていきます。
ステータスコード:429 Too Many Requests
エラーメッセージ:"Rate Limit Exceeded"
-
rate_limit_reset
フィールドが含まれる
以下の画像の通り、エラーレスポンスが返ってきたので、成功です。
以上で、テストは終了です!
まとめ
今回は、Go 言語の Fiber
を使用した URL Shortener の実装方法を解説しました。
Redis を使った高速データアクセス、レートリミットの設定、Postman を使った API テストまでをカバーし、シンプルかつ実用的な短縮URLシステムを構築しました。
最後まで読んでいただき、ありがとうございました!
参考
Discussion