Nuxt3のファイルアップロードをServer API Routeで行う
本文書ではNuxt3でのファイルのアップロード方法について説明を行っています。Nuxt3はフルスタックフレームワークなのでサーバ機能も備えています。そのためファイルのアップロード先にはServer API Routeを利用することができます。Server API Routeでのファイルの保存方法には複数の方法があるので 4つの方法で説明を行っています。4つの方法の中にmulter, formidableライブラリを利用した方法が含まれています。またファイルをJSON データで送信した方法も含まれています。
Nuxt3 の基本的な機能については下記の記事で公開しています。
目次
プロジェクトの作成
ファイルのアップロードを行う環境を構築するためにNuxt3のプロジェクトの作成を行います。プロジェクト名にはnuxt3-file-uploadという名前をつけていますが任意の名前をつけてください。
% npx nuxi@latest init nuxt3-file-upload
Need to install the following packages:
nuxi@3.6.3
Ok to proceed? (y) y
Nuxi 3.6.3 14:45:25
✨ Nuxt project is created with v3 template. Next steps: 14:45:26
› cd nuxt3-file-upload 14:45:26
[14:45:26] › Install dependencies with npm install or yarn install or pnpm install
[14:45:26] › Start development server with npm run dev or yarn dev or pnpm run dev
コマンドを実行後にプロジェクト名で指定した名前と同じディレクトリ名を持つ nuxt3-file-upload ディレクトリが作成されるので移動して npm install コマンドを実行します。
% cd nuxt3-file-upload
% npm install
アップロードフォームの作成
app.vue ファイルにアップロードのフォームを作成します。form タグの中に type 属性に file を設定した input 要素を追加しています。click イベントを設定してファイルを選択すると onChange 関数が実行されエンドポイント”/api/upload”にファイルが送信されます。
<template>
<div>
<h1>ファイルアップロード</h1>
<form>
<label for="file">File: </label>
<input type="file" name="file" @change="onChange" />
</form>
</div>
</template>
<script setup>
const onChange = async (e) => {
const files = e.target.files;
const formData = new FormData();
formData.append('file', files[0]);
await useFetch('/api/upload', {
method: 'post',
body: formData,
});
};
</script>
npm run dev コマンドを実行して開発サーバを起動します。
% npm run dev
ブラウザから localhost:3000 にアクセスするとファイルアップロード画面が表示されます。
Server API Route の設定
ファイルの送信先であるエンドポイントの/api/upload を設定するために server ディレクトリの下に api ディレクトリ、その下に upload.ts ファイルを作成します。
readMultipartFormData
送信されてくる Content-Type の multipart/form-data のデータからファイルを取得するために readMultipartFormData を利用します。Server Route API で利用しているサーバは unjs/h3 なので利用できる関数についてはunjs/h3 で利用できる関数で確認することができます。
export default defineEventHandler(async (event) => {
const files = await readMultipartFormData(event);
console.log(files);
return {
message: 'success',
};
});
ファイルアップロード画面からファイルをアップロードすると”npm run dev”コマンドを実行したターミナルにはファイルの情報が表示されます。
[
{
name: 'file',
filename: 'nuxt3-file-upload.png',
type: 'image/png',
data: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 04 b0 00 00 02 ad 08 02 00 00 00 b8 db 00 e7 00 00 00 19 74 45 58 74 53 6f 66 74 77 61 72 65 00 ... 20545 more bytes>
}
]
配列の中にデータが入っていることがわかったので、バリデーションを設定しておきます。送信されてきた情報にファイルが含まれていない場合にはヘルパー関数の createError 関数を実行します。今回はフォームではファイルを選択したらリクエストを送信するので今回の例では基本的にはこのエラーが実行されることはありません。
export default defineEventHandler(async (event) => {
const files = await readMultipartFormData(event);
if (!files || files.length === 0) {
throw createError({
statusCode: 400,
statusMessage: 'Image Not Found',
});
}
console.log(files);
return {
message: 'success',
};
});
application/json で送信されてきたデータは readBody 関数が利用することでデータを取り出すことができます。
public ディレクトリへのファイルの保存
files の中に配列でファイル情報が入っているので for loop で展開してファイルを保存します。ファイルを保存する際には Node の writeFile 関数を利用しています。public フォルダの中にアップロードしたファイル名のままで保存する設定としています。
import { writeFile } from 'fs/promises';
export default defineEventHandler(async (event) => {
const files = await readMultipartFormData(event);
if (!files || files.length === 0) {
throw createError({
statusCode: 400,
statusMessage: 'Image Not Found',
});
}
for (let i = 0; i < files.length; i++) {
if (files[i].name === 'file') {
const filename = files[i].filename;
// const mimetype = files[i].type;
const data = files[i].data;
const filePath = `./public/${filename}`;
await writeFile(filePath, data);
}
}
return {
message: 'success',
};
});
アップロードフォームからファイルを選択すると public ディレクトリにファイルが保存されることが確認できるはずです。
アップロードしたファイルは public ディレクトリに保存されているので下記のようにアクセスすることができます。本文書では nuxt3-file-upload.png という名前のファイルをアップロードしています。
<template>
<div>
<h1>ファイルアップロード</h1>
<form>
<label for="file">File: </label>
<input type="file" name="file" @change="onChange" />
</form>
<ul>
<li><img src="/nuxt3-file-upload.png" style="width: 200px" /></li>
</ul>
</div>
</template>
<script setup>
const onChange = async (e) => {
const files = e.target.files;
const formData = new FormData();
formData.append('file', files[0]);
await useFetch('/api/upload', {
method: 'post',
body: formData,
});
};
</script>
ブラウザからアップロードしたファイルを確認することができます。
multer を利用した場合
Node.js の Express でファイルをアップロードする際に利用される multer パッケージを利用した場合の動作確認を行います。
multer の設定については以下の記事が参考になります。
multer を利用するために multer のインストールを行います。
% npm install multer
multer の型もインストールします。
% npm install @types/multer
multer を利用するために h3 かえら callNodeListener 関数を利用します。TypeScript のエラーが表示されるので@ts-expect-error を設定しています。
import multer from 'multer';
import { callNodeListener } from 'h3';
const upload = multer({ dest: './public/' });
export default defineEventHandler(async (event) => {
// @ts-expect-error
await callNodeListener(upload.single('file'), event.node.req, event.node.res);
return {
message: 'success',
};
});
上記のコードでは TypeScript の allowSyntheticDefaultImport に関するエラーが表示されます。
Module '"/Users/mac/Desktop/nuxt3-file-upload/node_modules/@types/multer/index"' can only be default-imported using the 'allowSyntheticDefaultImports' flagts(1259)
index.d.ts(321, 1): This module is declared with 'export =', and can only be used with a default import when using the 'allowSyntheticDefaultImports' flag.
server ディレクトリの中に tsconfig.json ファイルに allowSyntheticDefaultImport の設定を行います。設定するとメッセージは表示されなくなります。
{
"extends": "../.nuxt/tsconfig.server.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true
}
}
上記の設定でフォームからファイルのアップロードを行うとランダムな名前のファイルが public ディレクトリに作成されます。ファイルの拡張子を設定すると画像ファイルとして表示されます。ここまでの設定でファイルのアップロードは完了です。
アップロードしたファイルの名前をファイル名として設定するために追加設定を行います。
import multer from 'multer';
import { callNodeListener } from 'h3';
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, './public/');
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
export default defineEventHandler(async (event) => {
// @ts-expect-error
await callNodeListener(upload.single('file'), event.node.req, event.node.res);
return {
message: 'success',
};
});
入力フォームからファイルをアップロードすると public ディレクトリの中にアップロードしたファイル名で保存されます。
formidable を利用した場合
Node.js 環境では formidable パッケージを利用してもファイルのアップロードを行うことができます。
formidable をインストールするために formidable パッケージとその型をインストールします。
% npm install formidalble @types/formidable
インストールした formidable 関数の引数にファイルを保存したディレクトリを指定します。ここでは”./public”ディレクトリを指定しています。form の parse メソッドの引数に event.node.req を指定します。
import formidable from 'formidable';
export default defineEventHandler(async (event) => {
const form = formidable({ uploadDir: './public/' });
const [fields, files] = await form.parse(event.node.req);
console.log(files);
return {
message: 'success',
};
});
フォームからファイルのアップロードを行うと files の情報がターミナルに表示されます。オブジェクトの情報が表示されると同時に自動に付与されている newFilename の値で public ディレクトリにファイルが保存されることが確認できます。
{
file: [
PersistentFile {
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
lastModifiedDate: 2023-07-15T13:29:36.798Z,
filepath: '/Users/mac/Desktop/nuxt3-file-upload/public/6bb4fda86387e71961d293d00',
newFilename: '6bb4fda86387e71961d293d00',
originalFilename: 'nuxt-file-upload-2.png',
mimetype: 'image/png',
hashAlgorithm: false,
size: 89271,
_writeStream: [WriteStream],
hash: null,
[Symbol(kCapture)]: false
}
]
}
file プロパティの中の配列にファイルの情報が含まれており、それらの情報を利用して自動ではなくアップロードしたファイル名で保存されるように追加の設定を行います。一度保存したファイルの名前を変更するために fs モジュールの rename メソッドを利用します。
import formidable from 'formidable';
import fs from 'fs';
export default defineEventHandler(async (event) => {
const form = formidable({ uploadDir: './public/' });
const [fields, files] = await form.parse(event.node.req);
if (Array.isArray(files.file)) {
files.file.forEach((file) => {
const fileName = `./public/${file.originalFilename}`;
fs.rename(file.filepath, fileName, (err) => console.log(err));
});
}
return {
message: 'success',
};
});
再度フォームからファイルのアップロードを行うとアップロードしたファイル名でファイルが保存されます。
JSON でのファイル送信
ここまではファイルを送信する際に formData を利用していましたがここでは JSON データとして送信します。
JSON としてデータを送信する前に files[0]にはどのような情報が含まれているか確認しておきます。
<template>
<div>
<h1>File Upload</h1>
<form>
<label for="file">File: </label>
<input type="file" name="file" @change="onChange" />
</form>
</div>
</template>
<script setup>
const onChange = async (e) => {
const files = e.target.files;
console.log(files[0]);
};
</script>
ファイル名やサイズ、ファイルタイプを確認することができます。
File {name: 'nuxt3-file-upload.png', lastModified: 1689402281206, lastModifiedDate: Sat Jul 15 2023 15:24:41 GMT+0900 (日本標準時), webkitRelativePath: '', size: 20595, type:"image/png"}
ファイルからのデータの読み込みは FileReader を利用します。読み込んだデータとファイル名などの情報は
<template>
<div>
<h1>File Upload</h1>
<form>
<label for="file">File: </label>
<input type="file" name="file" @change="onChange" />
</form>
</div>
</template>
<script setup>
const onChange = (e) => {
const files = e.target.files;
const reader = new FileReader();
reader.onload = async (event) => {
const fileData = {
name: files[0].name,
size: files[0].size,
type: files[0].type,
data: event.target.result,
};
await useFetch('/api/upload', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(fileData),
});
};
reader.readAsDataURL(files[0]);
};
</script>
Server Route API 側でデータを取り出すために readBody 関数を利用します。
export default defineEventHandler(async (event) => {
const body = await readBody(event);
console.log(body);
return {
message: 'success',
};
});
送られてきた画像の情報は以下の通りです。
{
name: 'nuxt3-file-upload.png',
size: 20595,
type: 'image/png',
data: 'data:image/png;base64,iVBORw0KGgoAAAANSU
//略
writeFile 関数を利用してファイルを保存します。split 関数を利用して data の中に含まれている”data/png;base64,“の文字列を data から取り除いてします。
png ファイルの場合は”data/png;base64,”, jpg ファイルの場合は”data/jpeg;base64,”, pdf ファイルの場合は”data/pdf;base64”などアップロードしたファイルによって値が異なります。
import { writeFile } from 'fs/promises';
export default defineEventHandler(async (event) => {
const body = await readBody(event);
console.log(body);
const filename = body.name;
const filePath = `./public/${filename}`;
const data = body.data.split(';base64,').pop();
await writeFile(filePath, data, 'base64');
return {
message: 'success',
};
});
public ディレクトリを確認するとアップロードしたファイルが保存されていることが確認できます。
Nuxt3 でのファイルのアップロード方法を確認することができました。