Closed9

100日チャレンジ day23 (画像リサイズアプリ)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/47bc6a1b772384


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

https://github.com/lirlia/100day_challenge_backend/tree/main/day23_image_resizer_api

riddle_tecriddle_tec

承知しました。案A(サンプル画像利用)に基づき、「実際に画像をリサイズするAPI」の仕様を固めます。

アプリケーション名: Day 23 - Image Resizer API

目的:
事前にサーバーの public ディレクトリに配置したサンプル画像(picsum.photos から取得したもの)のIDと、希望のサイズ(幅・高さ)をパスパラメータで受け取り、画像処理ライブラリ sharp を使って実際に画像をリサイズし、そのリサイズされた画像データをレスポンスとして返すAPIと、そのAPIをテストするための簡単なUIを提供します。

主要機能:

  1. 画像リサイズAPI エンドポイント:

    • パス: /api/images/[id]/[width]/[height]
    • メソッド: GET
    • パスパラメータ:
      • id: 事前に配置したサンプル画像のファイル名 (拡張子を除く。例: cat, mountain) (必須、文字列)
      • width: 希望する画像の幅 (必須、数値、1以上)
      • height: 希望する画像の高さ (必須、数値、1以上)
    • 処理:
      • 受け取った id から、public/images/ ディレクトリ内の画像ファイルパスを特定します (例: public/images/cat.jpg)。 注意: 拡張子は .jpg 固定とします。
      • fs.promises.access などでファイルが存在するか確認します。存在しない場合は、ステータスコード 404 Not Found とエラーメッセージ ({ message: "Image not found" }) をJSONで返します。
      • width, height が数値であり、かつ1以上であることをバリデーションします。無効な場合は、ステータスコード 400 Bad Request とエラーメッセージ ({ message: "Invalid width or height" }) をJSONで返します。
      • fs.promises.readFile で画像ファイルを読み込みます。
      • 画像処理ライブラリ sharp を使用し、読み込んだ画像データを指定された widthheight でリサイズします (sharp(buffer).resize(width, height).jpeg().toBuffer())。出力形式はJPEGとします。
      • リサイズ後の画像データ (Buffer) をレスポンスボディとして返します。
      • レスポンスヘッダーに Content-Type: image/jpeg を設定します。
      • レスポンスステータスコードは 200 OK とします。
    • エラーハンドリング: 画像の読み込み、リサイズ処理中に予期せぬエラーが発生した場合は、console.error でログを出力し、ステータスコード 500 Internal Server Error とエラーメッセージ ({ message: "Internal server error" }) をJSONで返します。
  2. テスト用UI:

    • パス: /
    • 機能:
      • タイトル (Day23 - Image Resizer API) を表示します。
      • 利用可能なサンプル画像のID (cat, mountain, abstract など、準備したファイル名から拡張子を除いたもの) を表示または選択できるようにします(例: ラジオボタンやドロップダウン)。
      • 幅 (width) と高さ (height) を入力する数値入力フィールドを設けます。
      • 「画像表示」ボタンなどを設置します。
      • ボタンクリック時、または入力値変更時に、選択/入力された id, width, height を使ってAPIエンドポイントのURL (/api/images/{id}/{width}/{height}) を動的に生成します。
      • 生成されたURLを <img> タグの src 属性に設定し、APIから返されるリサイズされた画像を表示します。
      • 初期表示やエラー時には、適切なメッセージやプレースホルダーを表示します。
  3. サンプル画像の準備:

    • picsum.photos から適当な画像を 3枚 ダウンロードします。
      • 例1: https://picsum.photos/seed/cat/600/400 -> cat.jpg
      • 例2: https://picsum.photos/seed/mountain/800/600 -> mountain.jpg
      • 例3: https://picsum.photos/seed/abstract/700/500 -> abstract.jpg
    • public/images/ ディレクトリを作成し、ダウンロードした画像を上記のファイル名 (cat.jpg, mountain.jpg, abstract.jpg) で保存します。

データモデル:

  • データベースは使用しません。prisma ディレクトリや関連ファイルは使用しませんが、テンプレートからコピーされるため残っていても問題ありません。

技術スタック:

  • フレームワーク: Next.js (App Router)
  • 言語: TypeScript
  • API: Next.js Route Handlers
  • 画像処理: sharp
  • スタイリング: Tailwind CSS
  • DB: なし

作業手順:

  1. プロジェクト初期化:
    • template ディレクトリをコピーして day23_image_resizer_api ディレクトリを作成します。
    • package.jsonname フィールドを day23_image_resizer_api に変更します。
    • README.md にこの仕様を記載します。
    • npm install sharp を実行して sharp をインストールします。
  2. サンプル画像準備:
    • public/images/ ディレクトリを作成し、上記の例のように picsum.photos からダウンロードした画像を3枚 (cat.jpg, mountain.jpg, abstract.jpg) 保存します。
  3. APIエンドポイント実装:
    • app/api/images/[id]/[width]/[height]/route.ts を作成し、上記の仕様通りに GET ハンドラを実装します。 fs/promisespath, sharp をインポートして使用します。
  4. UI実装:
    • app/page.tsx を編集し、上記の仕様に沿ったテスト用UIを実装します。useState でフォームの状態を管理し、利用可能な画像IDをハードコードします。
  5. 動作確認:
    • UIで各サンプル画像IDと様々な有効/無効なサイズを入力し、画像が正しく表示/エラーになることを確認します。
    • ブラウザの開発者ツールでAPIのレスポンスヘッダー (Content-Type) とステータスコードを確認します。
    • curl -v http://localhost:3001/api/images/cat/100/150 -o resized_cat.jpg のようなコマンドでAPIを直接叩き、画像ファイルがダウンロードできること、curl -v http://localhost:3001/api/images/invalid/100/100curl -v http://localhost:3001/api/images/cat/0/100 でエラーが返ることを確認します。
  6. ドキュメント更新:
    • README.md を最終確認・更新します。
    • (任意) .cursor/rules/knowledge.mdc に今回のアプリを追加します。

この仕様と手順で進めましょう。まずはプロジェクトの初期化から始めます。

riddle_tecriddle_tec

画像を表示ボタンをおしても表示されないバグ進行中

riddle_tecriddle_tec

本番で気をつけること

今回の画像リサイズAPIを本番運用する場合、PoCの段階では省略していた多くの点を考慮する必要があります。主な注意点は以下の通りです。

  1. セキュリティ:

    • 入力バリデーションの強化:
      • 画像ID: id パラメータに ../ などが含まれていないか厳格にチェックし、ディレクトリトラバーサル攻撃を防ぎます。許可する文字種(英数字、ハイフン、アンダースコアなど)を正規表現で制限したり、存在するファイルIDのリストと照合したりするべきです。現状の path.resolve だけでは不十分な場合があります。
      • サイズ: widthheight に現実的な上限値を設定します。非常に大きな値が指定されると、サーバーリソースを過剰に消費し、DoS攻撃につながる可能性があります。例えば、最大 4000x4000 ピクセルまで、といった制限を設けます。
    • レートリミット: 同一IPアドレスや(認証を導入する場合は)ユーザー/APIキーからのリクエスト数を一定時間内に制限します。これにより、ブルートフォース攻撃や過剰なリソース消費を防ぎます。(ioredis + Redis や express-rate-limit のようなライブラリを利用)
    • 認証/認可 (必要に応じて): 不特定多数に公開するAPIでなければ、APIキーやJWTトークンなどで認証を行い、許可されたクライアントのみが利用できるようにします。
  2. パフォーマンスとスケーラビリティ:

    • キャッシュの実装: これが最も重要です。同じ画像IDとサイズに対するリクエストが頻繁に来る場合、毎回リサイズするのは非効率です。
      • CDN: CloudFront, Cloudflare などのCDNを利用し、リサイズ結果をエッジサーバーにキャッシュさせます。適切な Cache-Control ヘッダーを設定します。
      • サーバーサイドキャッシュ: CDNを使わない/使えない場合や、さらにパフォーマンスを上げたい場合、Redis や Memcached などのインメモリキャッシュ、あるいはディスクキャッシュにリサイズ済み画像を保存し、次回以降のリクエストではキャッシュから返します。キャッシュキーは id, width, height, (必要ならフォーマットや品質も) を含めます。
    • リソース管理: sharp はCPUとメモリをそれなりに消費します。高負荷になった場合に備え、Node.js のクラスタモジュールを使ってプロセスを複数起動したり、コンテナ化 (Docker) して Kubernetes などでオートスケーリングできるようにしたりする構成を検討します。sharp.concurrency() で同時処理数を制限することも有効です。
    • 非同期処理/キューイング: リサイズに時間がかかる可能性がある場合(特に大きな画像や複雑な処理)、リクエストを受け付けたらすぐに 202 Accepted を返し、実際の処理はバックグラウンドジョブキュー (BullMQ, Celeryなど) に投入する方式も考えられます。処理完了後に結果を取得するエンドポイントを用意したり、Webhookで通知したりします。
  3. エラーハンドリングとロギング:

    • 詳細なエラー分類: sharp が返す可能性のあるエラーを細かくハンドリングし、クライアントに適切なエラー情報(ただし、内部情報は漏洩させない)を返します。
    • 構造化ロギング: リクエストID、ユーザー情報(認証導入時)、入力パラメータ、処理時間、エラー詳細などをJSON形式などでログ出力し、分析やデバッグを容易にします。
    • 監視とアラート: エラー発生率、レスポンスタイム、リソース使用率などを監視し、異常値を検知したら通知する仕組み (Prometheus + Grafana, Datadog, Sentryなど) を導入します。
  4. 画像処理オプション:

    • フォーマット指定: JPEG以外の出力フォーマット (PNG, WebP, AVIFなど) をクエリパラメータなどで指定できるようにし、Content-Type も動的に変更します。WebP/AVIFは多くの場合JPEGより効率的です。
    • 品質指定: JPEGやWebPの品質パラメータを指定できるようにします。
    • リサイズオプション: fit (cover, contain, fill, inside, outside) などのリサイズ方法を指定できるようにします。
    • メタデータ: EXIFなどのメタデータを保持するか削除するか選択できるようにします。
  5. ファイル管理 (サンプル画像以外の場合):

    • ユーザーがアップロードした画像を扱う場合は、S3などのオブジェクトストレージに保存する構成が一般的です。ファイル名の衝突回避、アクセス制御なども考慮が必要です。

特に、セキュリティ(入力バリデーション、レートリミット)パフォーマンス(キャッシュ) は、本番環境では必須と言える対策です。PoCから本番に移行する際は、これらの点を重点的に強化する必要があります。

このスクラップは4ヶ月前にクローズされました