Create Upload API with FeathersJS
Adam C. |

"File" is a special type of data submitting to the server. It's encoded as multipart/form-data (i.e., binary data.) It does not like simple key/value pairs from text fields, which could be captured in params or data context of FeathersJS, we would need middleware to covert this to either params or data, and then be saved to the server.

Photo by Ferran Feixas on Unsplash

Okay, let's build an Upload API with FeatherJS.

BTW - If you are new to FeathersJS, then please check their website here. Or a short tutorial I wrote a while ago.

NPM Install Multer

As I said, we need middleware. Not sure if there is another choice, but I believe Multer is the best. Multer is a node.js middleware for handling multipart/form-data, which is primarily used for uploading files.

Under your FeathersJS root directory, run 

npm install multer

Generate an Upload Service

FeatherJS CLI is really a life-saver. If you have not used it before, then you must try it out.  Under the root directory, run:

feathers generate service

Choose custom service, and named it something like, “upload”, and then http://localhost:3030/upload, will be your Upload API's endpoint. 

Modify the Upload Service

If you name your service “upload”, then you should see the new folder created under “ROOT/src/services”. The next is modifying some fields to integrate with Multer.

upload.service.js

Let's add middleware to transfer the received files to feathers.

const multer = require("multer");
const multipartMiddleware = multer();

module.exports = function (app) {
  // some options which cand be defined in config/default.json or production.json		
  const options = {
    fileServer: app.get("fileServer"),
    filePath: app.get("filePath"),
    imageMaxWidth: app.get("imageMaxWidth"), // for image upload only
    imageMinWidth: app.get("imageMinWidth"), // for image upload only
  };

  // Initialize our service with any options it requires
  app.use(
    "/upload",

    multipartMiddleware.array("files"),

    // another middleware, this time to
    // transfer the received files to feathers
    function (req, res, next) {
      req.feathers.files = req.files;
      next();
    },
    createService(options)
  );

  // Get our initialized service so that we can register hooks
  const service = app.service("upload");

  service.hooks(hooks);
};

Note that, if you only want to support single file upload, then you can use multpartMiddleare.single("file"). Obviously, multpartMiddleare.array("files") would receive the multiple files and save them in an array.

upload.class.js

Thanks to ‘Multer’, and now “files” are available in the params context. Note that req.feathers set the properties on the params instead of data  per FeathersJS doc

All middleware registered after the REST transport will have access to the req.feathers object to set properties on the service method params

If you really want the data of the files being saved in the data context, you can have a hook to convert it, but I don't see it's necessary.

Let's implement ‘create’ function in the upload.class.js to access/save the files uploaded.

async create(data, params) {

    const uploadedFiles = params.files; //note that the file in in params

    const allPromises = uploadedFiles.map(async (dataFile) => {
      const { originalname, buffer, mimetype } = dataFile;
      const { fileServer, filePath, imageMaxWidth, imageMinWidth } =
        this.options;

      const ts = new Date().getTime();

      const filename =
        ts +
        originalname
          .toLowerCase()
          .replace(/[^a-z0-9. -]/g, "") // remove invalid chars
          .replace(/\s+/g, "-") // collapse whitespace and replace by -
          .replace(/-+/g, "-");

      try {
        await uploadToLocal(
          filePath,
          filename,
          buffer,
          mimetype,
          imageMaxWidth,
          imageMinWidth
        );
      } catch (error) {
        console.log("Upload Error");
      }

      return {
        name: originalname,
        url: fileServer + "/" + filename,
        thumbnail: fileServer + "/sm-" + filename, //only True for image
      };
    });

    const responses = Promise.all(allPromises);
    return responses;

  }

Note every single file received contains the following attributes: originalname, buffer, and mimetype

const { originalname, buffer, mimetype } = dataFile;

Then you can save ‘buffer’ to anywhere we like, local or cloud. And in my case, I return the filename and URL. Note that the thumbnail attribute is for the image only.

uploadToLocal.js

You see I use ‘uploadToLocal’ function. which is something like this for your reference:

const sharp = require("sharp");
const fs = require("fs").promises;

const uploadToLocal = async (
  uploadFolderPath,
  fileName,
  fileBuffer,
  fileType,
  maxWidth,
  minWidth
) => {
  if (fileType && fileType.substr(0, 5).toLowerCase() === "image") {
    try {
      await sharp(fileBuffer, { withoutEnlargement: true })
        .resize(maxWidth)
        .toFile(uploadFolderPath + "/" + fileName);
      await sharp(fileBuffer, { withoutEnlargement: true })
        .resize(minWidth)
        .toFile(uploadFolderPath + "/" + "sm-" + fileName);
    } catch (error) {
      console.log(error);
      return false;
    }
  } else {
    try {
      await fs.writeFile(uploadFolderPath + "/" + fileName, fileBuffer);
    } catch (error) {
      console.log(error);
      return false;
    }
  }

  return true;
};

module.exports = uploadToLocal;

This should be straightforward, just note that my uploader also does resize for image files, which uses another NPM package - sharp

That's it, and you should be able to upload the File via this API now. Please leave the comments below if you have any questions or you would like to see how the client code looks like and I will write the article about that when I get a chance.