Async file upload with NextJS

I'm currently playing around with NextJS. My background is clearly in the Microsoft environment and for a few years 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 has questionable quality. But let's 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 as easy as expected.

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

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

Fine, I thought, let's find a library that is capable of doing 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 were: "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 were just uploaded by some dudes 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 file upload will not work.

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

Then 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 would 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;