Next.jsで画像ファイルをサーバー内に保存する方法【Next.js・TypeScript・formidable】

「Next.js・画像保存」で調べてもなかなかそれっぽい記事が見つからなくて苦労したのでうまくいったものをここに書きます。

# 環境構築

以下を実行してプロジェクトを作成

```terminal
npx create-next-app nextjs-image-upload --typescript
```

以下のサイトを参考に`Chakra UI`をインストール
（※今回は`Chakra UI`をフロント部分で使用しているため）

https://fwywd.com/tech/next-chakra-ui

`formidable`をインストール

FormDataを変換するのに必要

```terminal
npm i formidable
npm i @types/formidable
```

`swiper`をインストール
```terminal
npm install swiper
```

主に`pages`フォルダを触っていきます。

# フロント部分・データ送信


`Chakra UI`を使用するために`_app.tsx`を編集

```TypeScript:_app.tsx
import "../styles/globals.css";
import { ChakraProvider } from "@chakra-ui/react";
import type { AppProps } from "next/app";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ChakraProvider>
      <Component {...pageProps} />
    </ChakraProvider>
  );
}

export default MyApp;

```

フォーム画面とデータ送信

`swiper`選択画像のプレビュー表示をスライドにしている

`onSubmit`関数が送信処理

この中でFormDataを作成してファイルなどを入れてfetchでPOST送信している

```TypeScript:index.tsx
import {
  Container,
  FormLabel,
  Heading,
  Image,
  Input,
} from "@chakra-ui/react";
import type { NextPage } from "next";
import { ChangeEvent, FormEvent, useRef, useState } from "react";
import { Swiper, SwiperSlide } from "swiper/react"; //カルーセル用のタグをインポート
import { Pagination, Navigation } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation"; // スタイルをインポート
import "swiper/css/pagination"; // スタイルをインポート

const Home: NextPage = () => {
  const [images, setImages] = useState<Blob[]>([]);
  const inputNameRef = useRef<HTMLInputElement>(null);
  const inputFileRef = useRef<HTMLInputElement>(null);

  // 以下送信処理
  const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log("送信");

    const name = inputNameRef.current?.value

    const formData = new FormData();
    
    for await(const [i, v] of Object.entries(images)) {
      formData.append('files' , v);
    }
    formData.append("name", name || "");


    const post = await fetch(`${window.location.href}api/upload`, {
      method: "POST",
      body: formData,
    }); 

    console.log(await post.json());
  };

  const handleOnAddImage = (e: ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files) return;
    setImages([...e.target.files]);
  };

  return (
    <Container pt="10">
      <Heading>Image Form</Heading>
      <form onSubmit={onSubmit} encType='multipart/form-data'>
        <FormLabel htmlFor="postName">名前</FormLabel>
        <Input
          type="text"
          id="postName"
          placeholder="Name"
          size="lg"
          ref={inputNameRef}
        />
        <FormLabel htmlFor="postImages">画像</FormLabel>
        <Input
          type="file"
          id="postImages"
          multiple
          accept="image/*,.png,.jpg,.jpeg,.gif"
          onChange={handleOnAddImage}
          ref={inputFileRef}
        />
        <Input type="submit" value="送信" margin="10px auto" variant="filled" />
      </form>
      <Container>
        <Swiper
          slidesPerView={1} //一度に表示するスライドの数
          modules={[Navigation, Pagination]}
          pagination={{
            clickable: true,
          }} //　何枚目のスライドかを示すアイコン、スライドの下の方にある
          navigation //スライドを前後させるためのボタン、スライドの左右にある
          loop={true}
        >
          {images.map((image, i) => (
            <SwiperSlide key={i}>
              <Image
                src={URL.createObjectURL(image)}
                w="full"
                h="40vw"
                objectFit="cover"
              />
            </SwiperSlide>
          ))}
        </Swiper>
      </Container>
    </Container>
  );
};

export default Home;

```

# サーバー側のAPI・画像を取得と保存

`pages/api`フォルダの中に`upload.ts`ファイルを作成する

FormDataで送信された内容を`formidable`で解析する

日本語の情報が少なかった、あっても古かったり…

`/public/images`ディレクトリがないと正常に動かないので作成すること、ここに画像が保存される。

```TypeScript:upload.ts
import type { NextApiRequest, NextApiResponse } from "next";
import formidable from "formidable";
import { createWriteStream } from "fs";

export const config = {
  api: {
    bodyParser: false,
  },
};
type Data = {
  msg?: string;
};
export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  if (req.method !== "POST") return;

  const form = formidable({ multiples: true, uploadDir: __dirname });

  form.onPart = (part) => {
    
    // let formidable handle only non-file parts
    if (part.originalFilename === "" || !part.mimetype) {
      // used internally, please do not override!
      form._handlePart(part);
    } else if (part.originalFilename) {

      // 以下でファイルを書き出ししている      

      console.log(part.name);
      // /public/imagesディレクトリがないと正常に動かないので作成すること
      const path =
        "./public/images/" + new Date().getTime() + part.originalFilename;
      const stream = createWriteStream(path);
      part.pipe(stream);

      part.on("end", () => {
        console.log(part.originalFilename + " is uploaded");
        stream.close();
      });

    }
  };

  // input[type="file"]以外の値はここから見れた
  form.on('field', (name, value) => {
    console.log(name);
    console.log(value);
  })

  // これを実行しないと変換できない
  form.parse(req)

  // これでもinput[type="file"]以外の値はここから見れるが、fileは見れない
  // form.parse(req, async (err, fields, files) => {
  //   console.log("fields:", fields); // { name: '*'}
  //   console.log("files:", files); // {}

  //   res.status(200).json({ name: "!!!" });
  // });

  // レスポンス
  res.status(200).json({ msg: "success!!" });
}

```