GreenSnap TECH BLOG

GreenSnapのエンジニアチームの取り組みや使っている技術を紹介します

Next.js(TypeScript) / Axios /SpringBootでmultipart/form-dataを送信する

はじめに

今年、約4年振りにGreenSnapに舞い戻ってきた伊藤です。 以前はサーバーサイド(Scala)と、iOS(Swift)の開発をしていましたが、戻ってきてからはサーバーサイドエンジニアとして、ScalaとKotlinをメインに開発しております。

今回のテーマ

業務で開発しているサービスでお問い合わせ画面を開発する機会があり、 その際に添付ファイルをアップロードする必要があった為、表題の通りNext.jsとSpringBootを使ってファイルアップロードを実装してみたいと思います。 割と簡単かと思いますが、随所にハマりポイントがあったので、誰かのお役に立てばと思います。

今回使用する技術スタックは次の通りです。

  • Next.js(TypeScript) 12.3.0

  • Axios 0.27.2

  • Sprint Boot 2.6.3

今回作るもの

よくある画面だと思いますが、名前とメールアドレス、それに加えて画像ファイルを添付できるお問い合わせ画面です。 添付する画像ファイルは複数枚対応できるものとします。

画面イメージは次の感じです。

ソースコード

実際のソースコードです。

Next.js側

ページ(/pages/FileUploadTest.tsx)

まずはNext.jsの問い合わせページのコードです。 formを設置して、適当に各input要素を置いてます。

import {ChangeEvent, FormEvent, useState} from "react";
import axios from "axios";

export default function FileUploadTest() {

  const [name, setName] = useState<string>('')
  const [mailAddress, setMailAddress] = useState<string>('')
  const [images, setImages] = useState(new Map<string, File>())

  return (
    <form method={'post'} onSubmit={(e) => register(e)}>
      <p>
        名前: 
        <input type={'text'} value={name} required={true}
                     onChange={(e) => setName(e.target.value)}></input>
        メールアドレス: 
        <input type={'email'} value={mailAddress} required={true}
               onChange={(e) => setMailAddress(e.target.value)}></input>
      </p>
      <hr/>
      <input id="file01" type="file" accept="image/jpeg, image/png" onChange={(e) => handleUpload(e)}/>
      <input id="file02" type="file" accept="image/jpeg, image/png" onChange={(e) => handleUpload(e)}/>
      <hr/>
      <button type={'submit'}>アップロード</button>
    </form>
  )

  async function handleUpload(e: ChangeEvent<HTMLInputElement>) {
    if (!e.target.files) return
    const file = e.target.files[0]

    setImages(before => new Map(before.set(e.target.id, file)))
  }

  async function register(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const formData = new FormData() // axiosへ渡すデータとしてFormDataを使用
    formData.append('name', name)
    formData.append('mailAddress', mailAddress)
    images.forEach((image, key) => formData.append('files', image)) // filesというキーでFileを複数セット

    await axios.post(
      `/api/fileUploadTest/upload`, // Next.jsのAPI Routesを呼ぶ
      formData,
      { headers: {'Content-Type': 'multipart/form-data'}} // contentTypeに 'multipart/form-data' を指定
    ).then(response => {
      alert('アップロードしました')
    }).catch(error => {
      alert('エラーが発生しました')
    })
  }

}

ポイントとしては、

  1. FormDataを使用する
  2. axiosでNext.jsのAPI Routesを呼ぶ(コードは後述)
  3. axios.post時にcontentTypeに multipart/form-data を指定する

でしょうか。

API Routes(/pages/api/fileUploadTest/upload.ts)

次にページから呼ばれるNext.jsのAPI Routesのコードです。

ページからリクエストを受け取り、サーバ側(SpringBoot)のエンドポイントにリクエストを投げます。

import type { NextApiRequest, NextApiResponse } from 'next'
import axios from "axios";

export const config = {
  api: { bodyParser: false } // bodyParserを無効にする
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).end()
  }

  axios.post(
    `http://localhost:8080/api/fileUploadTest/upload`, // SpringBoot側のエンドポイント
    req,
    // { headers: {'Content-Type': 'multipart/form-data'}} // ← NG
    { headers: {'Content-Type': req.headers["content-type"]}}
  )
    .then(response => {
      if (response.status === 200) {
        res.status(200).json({ status: 'OK' })
      } else {
        res.status(500).json({ status: 'NG' })
      }
    })

}

ポイントとしては、

  1. bodyParseを無効にする
  2. axios.post時にcontentTypeに req.headers["content-type"] を指定する

です。

bodyParserを無効にしないと、ページから渡ってきたmultipart/form-data を上手く解釈できません。 また、サーバ側へのcontentTypeにはreq.headers["content-type"]を指定する必要があります。

この時、contentTypeを指定しなかったり、multipart/form-data を再指定したりすると、SpringBoot側へ正しいリクエストとして届かずエラーになります。 自分はここに気づくのに結構時間を取られました。

SpringBoot側

最後にSpringBootで書いてあるサーバサイドのControllerです。

FileUploadTestController.kt
import org.springframework.http.HttpEntity
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

@RestController
@RequestMapping("/api/fileUploadTest")
class FileUploadTestController {

    @PostMapping("/upload")
    fun upload(
//        @RequestParam("name") name: String, RequestParamでは文字化けする
        @RequestPart("name") name: String,
        @RequestParam("mailAddress") mailAddress: String,
        @RequestPart("files") files: List<MultipartFile>?
    ): HttpEntity<*> {
        println(name)
        println(mailAddress)
        println(files?.get(0)?.originalFilename)
        println(files?.get(1)?.originalFilename)
        return ResponseEntity.EMPTY
    }
}

ポイントとしては、

  1. name, mailAddressなどのテキストデータは@RequestPart または@RequestParamで受け取る
  2. nameなどマルチバイト文字を@ RequestParamで受け取ると(なぜか)文字化けする
  3. ファイルは@RequestPartで受け取りMultipartFileとして処理する

です。

まとめ

以上簡単ですが、Next.jsとAxiosとSpringBootでmultipart/form-dataを送信する方法でした。 調べれば、それなりに情報は出てくるのですが、(自分調べでは)断片的なものばかりで、色々な情報を組み合わせて実装するのに大変だったので、今回記事にしてみました。

最後に

弊社では絶賛エンジニア募集中です。BtoCのサービス開発をしてみたい方や、植物に興味のある方は是非応募してください。 カジュアルに話だけでも聞きたいという方もお待ちしてます。 www.wantedly.com