React/Express/TypescriptでZIPファイルをアップロードする方法

Express Node.js React

Reactアプリケーションでファイルのアップロードを実現する方法は多くありますが、ZIPファイルのアップロードに特化した方法に焦点を当てたいと思います。
この記事では、React/TypeScript/Expressを使用してZIPファイルをアップロードするための基本的なステップを紹介します。
ファイルの選択からアップロード、バックエンドの処理まで、ステップバイステップで説明します。

目次

  1. 環境
  2. 構成
  3. プロジェクトのセットアップ
  4. フロントエンドUIとファイル選択の実装
  5. アップロード処理の実装
  6. バックエンドの基本的な実装
  7. ZIPファイルの処理
  8. CORSエラー対策
  9. 終わりに

環境

  • macOS Catalina (10.15.7)
  • node v14.17.1
  • npm v6.14.13
  • React v18.2.0
  • TypeScript(Front-end) v4.9.5
  • Axios v1.5.1
  • ChakraUI v2.8.1
  • Express v4.18.2
  • TypeScript (Back-end) v5.2.2

構成

フロントエンドをReact/TypeScriptで構成し、ZIPファイル送信にAxiosを使用しています。
UIはChakraUIを使用して実装を行なっています。
バックエンドはExpress/TypeScriptで構成しています。
ファイル作成はtouchコマンド、ディレクトリ作成はmkdirコマンド、移動はcdコマンドで行なっていきます。

作成済みプロジェクトのリンクは以下になります。
https://github.com/altoSand/fileUpload

プロジェクトのセットアップ

プログラムを配置する任意のフォルダーを作成します。
今回はfileUploadという名前にしました。
コマンドを実行し、TypeScriptでのフロントエンドのReactプロジェクトの作成を行います。

npx create-react-app frontend --template typescript

バックエンド用フォルダーとしてbackendを作成し、移動後に次のコマンドを実行します。

npm init -y

作成後のファイル構造は以下のようになります。(LICENCEは気にしなくてOK)

フロントエンドUIとファイル選択の実装

このセクションでは下の画像のようなファイル選択UIを目指してUIフレームワークを活用して実装していきます。(めんどいよって人はinputとbuttonだけでもたぶんいけるよ!)

最初にfrontendに移動してフロントエンドにChakuraUIをインストールしていきます。
ChakraUIの公式サイトはこちらから。

npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion

インストールが成功したらsrcのindex.tsxを開き、ChakraUIを使用するためにChakraProviderをimportし、<App />を囲みます。(抜粋コードは下記)

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { ChakraProvider } from "@chakra-ui/react";

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <ChakraProvider>
      <App />
    </ChakraProvider>
  </React.StrictMode>
);

次にsrcのApp.tsxを開き、return内の既存コードをChakuraUIを使用したものへと書き換えていきます。
今回はファイルアップロードのUIを画面の真ん中に配置したいので、Flexコンポーネントに対して下のコードのような指定を行います。
その中にCardコンポーネントを入れれば真ん中に配置することができます。
Cardコンポーネントの中にファイルを選択するためのInputにファイルのための指定をし、アップロードボタン、見出しのCardHeadingを配置します。
最後に使用したChakraUIにコンポーネントのimportを行います。
実装後のコードは下記のようになります。

import React from 'react';
import logo from './logo.svg';
import './App.css';
import { Card, CardHeader, CardFooter, Heading, Flex, Button } from '@chakra-ui/react';

function App() {
  return (
    <Flex justify='center' align='center' h='100vh'>
      <Card align='center'>
        <CardHeader>
          <Heading size='md'>ファイルアップロード</Heading>
        </CardHeader>
        <CardFooter>
          <input type='file' accept='application/zip'/>
          <Button colorScheme='blue'>アップロード</Button>
        </CardFooter>
      </Card>
    </Flex>
  );
}

export default App;

これで最初の画像のようなUIが実装できたかと思います。
確認するにはコマンドでfrontendに移動後npm startをするとサーバーが起動し、確認できるようになります。

アップロード処理の実装

アップロード処理にはAxiosを使用するので、Axiosを導入していきます。
コマンドでfrontendに移動後に下記のコマンドを実行します。

npm install axios

Axiosのインストール後にファイルを選択し、stateに入れるための関数selectFile、アップロードを行うためのfileUpload関数を作成します。
まず、ReactのuseStateを使いfileを保持しておくstateを定義します。

import React, { useState } from 'react';
const [file, setFile] = useState<File | null>(null);

selectFile関数を定義していきます。
ここでeに何も指定しない場合にはanyタイプだぞと怒られるので気をつけましょう。
zipDataもチェックをしない場合にもzipDataがnullの可能性があるとエラーになります。

  const selectFile = (e: React.ChangeEvent<HTMLInputElement>) => {
    const zipData = e.target.files
    if (zipData && zipData[0]) { 
      setFile(zipData[0])
    }
  }

関数定義後にinputのonChangeに対してselectFileを指定します。

<input type='file' accept='application/zip' onChange={selectFile}/>

fileUpload関数はstateからデータを取り出し、それをaxiosを使用してバックエンドに送信します。
fileをif文でチェックしformDataにappendします。
ファイルを送る際にはヘッダーのコンテントタイプを指定するのを忘れないようにしましょう。
バックエンドのportは8000番にする予定なのでaxiosで送信urlはポートとAPIを指定します。

  import axios from 'axios';  
  const API_BASE_URL = 'http://localhost:8000';
  const fileUpload = () => {
    if(file) {
      const formData = new FormData();
      formData.append("file", file);
      axios({
        method: 'POST',
        headers: { 'Content-Type': 'multipart/form-data' },
        url: API_BASE_URL+'/upload',
        data: formData,
        withCredentials: true
      })
        .then(response => {
          console.log(response.data)
        })
        .catch(err => {
          console.log(err)
        })
    } 
  }

関数をButtonのonClickに割り当てます。

<Button colorScheme='blue' onClick={fileUpload}>アップロード</Button>

これでフロントエンド側の実装は以上になります。

バックエンドの基本的な実装

バックエンドにtypesciptを導入するためにbackendに移動後に下のコマンドします。

npm install typescript @types/node@20.7.2

Expressをインストールするために下のコマンドを実行します。

npm install express @types/express

次にTypeScriptコードを実行するためのts-nodeとNode.hsアプリケーションを自動で再起動するためのnodemonをインストールするために下のコマンドを実行します。

npm install ts-node nodemon

ライブラリのインストールが終わったらExpressを実行するserver.tsをbackend直下に作成し、package.jsonの設定をします。
作成後にpackage.jsonのmainをコードのように変更し、scriptsの中身を追加します。
変更後のpackage.jsonは下のようになります。

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "server.ts",
  "scripts": {
    "start": "npx nodemon server.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
}

TypeScriptの設定します。下記のコマンドを実行し、tsconfig.jsonを生成します。

npx tsc --init

tsconfigではtarget:”ES6″に変更します。ここらへんは好みかと思います。
server.tsにExpressの基本的な要素を記述したものが以下になります。

import express from 'express';

const app: express.Express = express();
const port = 8000;

app.use(express.json());

app.post('/upload', (req: express.Request, res: express.Response) => {
    res.send(200).send();
});

app.listen(port, () => {
    console.log(`サーバーがポート ${port} で起動しました。`);
});

バックエンドの基本的な実装は以上になります。

ZIPファイルの処理

ZIPファイルを処理するためにmulterのインストールを行います。
下記のコマンドを実行します。

npm install multer @types/multer

受信したZIPファイルの保存場所と保存する名前、受け取り次の処理を追加します。
保存先としてbackend直下にpublic/uploadDataを作成します。
追加後のserver.tsは下のようになります。

import express from 'express';
import multer from 'multer';

const app: express.Express = express();
const port = 8000;
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, './public/uploadData')
    },
    filename: (req, file, cb) => {
        cb(null, Date.now() + '-' + file.originalname);
    }
})
const upload = multer({ storage: storage });

app.use(express.json());

app.post('/upload', upload.single('file'), (req: express.Request, res: express.Response) => {
    if (!req.file) {
        return res.status(400).send('アップロードに失敗しました。');
    }
    console.log('成功');
    res.send(200).send();
});

app.listen(port, () => {
    console.log(`サーバーがポート ${port} で起動しました。`);
});

これでZIPファイルの受け取り処理ができました。

CORSエラー対策

ここまででフロントエンドでZIPファイルの送信、バックエンドでZIPファイルの受信するための処理を書いてきましたが、このまま実行してしまうと下の画像のようなエラーが発生してしまいます。
両方をnpm startで起動してみましょう。
ディベロッパーツールで確認してみると下の画像ようなCORSエラーが発生しています。

CORSエラーを解決するためには、許可するoriginにフロントエンドを追加する必要があります。
そのために、corsを設定するためのライブラリをインスールします。バックエンドの停止後に下記のコマンドを実行します。

npm install cors @types/cors

インストール後にserver.tsに設定を行います。
設定の後は下のようになります。

import express from 'express';
import multer from 'multer';
import cors from 'cors';

const app: express.Express = express();
const port = 8000;
const FRONT = ['http://localhost:3000'];

const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, './public/uploadData')
    },
    filename: (req, file, cb) => {
        cb(null, Date.now() + '-' + file.originalname);
    }
})
const upload = multer({ storage: storage });

app.use(
    cors({
        origin: FRONT,
        credentials: true
    })
);

app.use(express.json());

app.post('/upload', upload.single('file'), (req: express.Request, res: express.Response) => {
    if (!req.file) {
        return res.status(400).send('アップロードに失敗しました。');
    }
    console.log('成功');
    res.send(200).send();
});

app.listen(port, () => {
    console.log(`サーバーがポート ${port} で起動しました。`);
});

これでZIPファイルが保存できるようになったかと思います。

終わりに

本記事でReact/Express/TypeScriptでのZIPファイルをアップロードし、処理する方法についての基本的なステップについて解説を行いました。
開発の参考になれば幸いです。