はじめに
今年、約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('エラーが発生しました') }) } }
ポイントとしては、
- FormDataを使用する
- axiosでNext.jsのAPI Routesを呼ぶ(コードは後述)
- 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' }) } }) }
ポイントとしては、
- bodyParseを無効にする
- 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 } }
ポイントとしては、
- name, mailAddressなどのテキストデータは
@RequestPart
または@RequestParam
で受け取る - nameなどマルチバイト文字を
@ RequestParam
で受け取ると(なぜか)文字化けする - ファイルは
@RequestPart
で受け取りMultipartFile
として処理する
です。
まとめ
以上簡単ですが、Next.jsとAxiosとSpringBootでmultipart/form-dataを送信する方法でした。 調べれば、それなりに情報は出てくるのですが、(自分調べでは)断片的なものばかりで、色々な情報を組み合わせて実装するのに大変だったので、今回記事にしてみました。
最後に
弊社では絶賛エンジニア募集中です。BtoCのサービス開発をしてみたい方や、植物に興味のある方は是非応募してください。 カジュアルに話だけでも聞きたいという方もお待ちしてます。 www.wantedly.com