I'm currently playing around with NextJS. My background is clearly in the Microsoft environment and since a few year Angular, so I practice web development with TypeScript.

I had a hard time when I tried to implement a file upload with a NextJS API endpoint. The problems had basically nothing to do with NextJS, but with Node and its huge node module base which sometimes questionable quality. But lets start with how I used to implement NextJS endpoints. I'm using the next-connect for implementing and routing the endpoints.

const handler = nextConnect<NextApiRequest, NextApiResponse>();

handler.post(async (req, res) => {
  rest.status(200).end();
});

export default handler;

Since I'm using TypeScript, I want to leverage the async await language feature which simplifies the handling of asynchronous logic massively. But together with the callback approach of Node, it turns out to be not that easy as expected.

I first had a look, which libraries for file upload handling are available:

First I had a try with busboy because it allows to handle the file upload without temporary files. A first implementation with Node callbacks worked. However, I did not manage to collect and return the file ruls in the callbacks. I still don't know if I'm to stupid or if the library has a bug, but any shared variable updates to collect the url's did not work for me.

Fine, I thought, let's find a library which is capable to do the asynchronous handling with Promises.

The await-busboy library looked quite good, but it does not provide TypeScript type definitions. Implementing them yourself is a lot of work, especially if the API changes. So I tried out async-busboy. The advantage of a file upload handling without temp files is already gone with the async feature, but better than a not working solution. I implemented the code, which looked quite nice to me. But when I tried it out, it kept hanging while parsing the file upload. After researching I found this github issue https://github.com/m4nuC/async-busboy/issues/42. My first thoughts where: "it cannot be that there is no fucking stable library to handle file uploads in Node!"

Seriously, there are so many Node modules out there and a lot of them where just uploaded from some dude's without testing and any quality behind it. When developing a Node app, you are forced to assemble your application with a patchwork of libraries, from which you don't know its quality and future maintenance. I consider that as a big risk for productive applications.

Nevertheless, I gave formidable a try. Even though there is no async version of this library I managed to promisify it and use it in the async world. Here is my final and working solution.

First of all, we need to switch off the body parsing from NextJS otherwise the fileupload will not work.

export const config = {
  api: {
    bodyParser: false,
  },
};

Than I wrote a wrapper around formidable which returns a promise instead of using callbacks in my main code.

const parseForm = (req: IncomingMessage): Promise<[Fields, Files]> => {
  const form = new IncomingForm({ keepExtensions: true, allowEmptyFiles: false, multiples: true });

  return new Promise<[Fields, Files]>((resolve, reject) => {
    form.parse(req, (err, fields, files) => {
      if (err) reject(err);
      else resolve([fields, files]);
    });
  });
};

And than i yould implement my file upload endpoint, by using async await.

const handler = nextConnect<NextApiRequest, NextApiResponse>();

handler.post(async (req, res) => {

  const contentType = req.headers['content-type'];

  if (!contentType || contentType.indexOf('multipart/form-data') < 0) {
    res.status(HttpStatus.BAD_REQUEST).end();
    return;
  }

  try {
    const [, fileResult] = await parseForm(req);

    const files = Array.isArray(fileResult.file) ? fileResult.file : [fileResult.file];
    if (!validateFiles(files)) {
      res.status(HttpStatus.BAD_REQUEST).end();
    }

    const images = await Promise.all(
      files.map(uploadFilesToFirebaseStorage),
    );

    res.status(HttpStatus.CREATED).json(images);
  } catch (e) {
    res.status(HttpStatus.INTERNAL_SERVER_ERROR).end();
  }
});

export default handler;