NextJSのAPI Routeを使ったファイルアップロードについて

  • #開発
  • #NextJS
  • #API Route

こんにちは。CURUCURUエンジニアの長尾です。 先日、NextJSのファイルアップロードで結構時間をとられてしまったのでそれについて共有したいと思います。


目的

NextJSにおいてAPIルートを使ったファイルアップロードを行い、Nextのサーバ側で受け取ったファイルをバックエンドAPIへPOSTすること。

NextJSのファイルアップロード

NextJSにおいてファイルアップロードということに関しては2種類の方法があります。

  1. クライアントから直接バックエンドAPIへ送ったり、S3へアップロードしたりする方法
  2. クライアントからNextJSのAPI Routesへファイルアップロードを行い、そこからさらにバックエンドもしくはS3などへアップロードする方法

なぜ

なぜわざわざAPIRoutesを通すのか? それは以下の理由からです。

  • クライアントでの処理(postデータの整形など)を最小限にすることができる。
  • クライアントの負荷を減らす。サーバ側での処理を増やすことでユーザ端末依存による問題の解消につながる。
  • API Routesで起きたエラーなどはログを出しておくと検知ができる。(※クライアントではエラー検知できないかというと、Sentryでクライアントエラーを検知することは可能)

どうやるのか

1. 今回はformidable,form-data, fsというパッケージを使ってクライアントからのアップロードを処理しますので、以下でパッケージをインストール。

npm i formidable @types/formidable form-data fs
  • formidable:nodejsでフォームデータを受け取るパッケージ
  • form-data: multipart/form-dataを生成できるパッケージ

2. formidableをasync awaitを使って書けるようにPromiseでラップします。

formidableはAPIRoutesのrequestを渡すと、error, fields, filesを返してくれます。 errorはエラー情報。 fieldsはフォームでPOSTされたファイル以外のデータがstringで入ってきます。 filesはフォームでPOSTされたファイルが入ってきます。

const parseForm = (req: NextApiRequest): Promise<{ fields: formidable.Fields, files: formidable.Files }> => {
    return new Promise((resolve, reject) => {
        const form = new formidable.IncomingForm();
        form.parse(req, (err, fields, files) => {
            if (err) {
                reject(err);
            } else {
                resolve({ fields, files });
            }
        });
    });
};

3. formidableから受け取ったデータを使って、バックエンドへさらにポストするように実装します。

import fs from 'fs';

import FormData from 'form-data';
import formidable from 'formidable';

...省略
const postData = async (req: NextApiRequest, res: NextApiResponse<ErrorResponse | null>) => {
    const aw = axiosWrapper();
    // formidableでパースしたデータを取得
    const { files, fields } = await parseForm(req);
    // form-dataのFormDataを初期化
    const formData = new FormData();
    // データの整形処理(postデータをそのまま受け取って使える場合は
    const param = formatPostData(fields);
    // formDataに追加
    Object.entries(param).forEach(([key, value]) => {
        formData.append(key, value);
    });
    // filesの中にアップロードしたときのname属性でデータが入ってくる。anyにしてるのは仕方なく。。。。
    const f = files['file'] as any;
    if (f) {
        // ファイルがアップロードされていればformidableがfilepathに一時的に保存しているので読み出す
        const ff = fs.readFileSync(f['filepath']);
        // 読み出したデータをformDataに追加。formidableがoriginalFilenameに名前を保存している
        formData.append('file', ff, f['originalFilename']);
    }
    await aw.postFormData(formData);
    res.status(200).json(null);
};

※ axiosWrapperは適当な名前ですが、実際の実装ではaxiosをラップして使いやすくしたものを定義していますのでよしなに汲み取ってください。

4. 送ったデータをgoで受け取る

ginでの場合はFormFileを使うことで送られてきたデータを受け取ることができます。

func FormFile(ctx *gin.Context, key string) (multipart.File, *multipart.FileHeader, error) {
	return ctx.Request.FormFile(key)
}

// 使う側のイメージ
file, fileHeader, err := FormFile(ctx, "file")

いかがでしたでしょうか? 結構ニッチな部分ですが、APIRoutesでのファイル処理は割とありそうなので今回のテーマにしてみました。

以上、長尾がお届けしました。