Airtable is a great service for developers to to easily create databases prototypes, small side projects, budget conscience clients and more.

Airtable provides an easy to use API and a great interactive documentation on using this API. I’m going to NodeJS + ExpressJs to show you how to upload images via the api when creating a record. This can also be applied to updating a record.

I’m not going to go in-depth on how to setup NodeJS + ExpressJs so this post assumes that you are comfortable with Javascript and working with frameworks.

In Airtable I have created my base which is a simple database for recording information on talent and models. 

I have my form in my NodeJs which includes two (2) form fields one to accept images for headshots and the other accepts images for full body shots.

The Problem

Airtable doesn’t allow you to directly upload images via their api. They do, however, allow you to pass an object of attachments urls. This, however, means that the images need to be on a web server and therefore won’t work from local development environment.

The Solution

I am going to use Cloudinary to host the images which will give me the necessary urls to pass to Airtable. Cloudinary is a great service to host images and video and they have a generous free plan that should be suitable for small projects.

You can also use Filestack, but it is a bit pricey and doesn’t have a free plan but a 14 day free trial.

See the Pen How to upload images to Airtable using its API by Chinara James (@cjwd) on CodePen.0

Let’s break it down

In index.js we have set up our route, in my case when the user visits /models. This calls 3 functions we have set up in our Model Controller (modelController.js).

We require the following packages and these are declared at the top of modelController.js file.

We first need to set up options for multer. We are storing the images in memory and ensuring the uploaded files are indeed images.

let multerOptions = {
  storage: multer.memoryStorage(),
  fileFilter(req, file, next) {
    const isPhoto = file.mimetype.startsWith('image/');
    if(isPhoto) {
      next(null, true);
    } else {
      next({ message: "That filetype isn't allowed"}, false);
    }
  }
};

In our upload function we are telling multer what the names of our upload fields are and optionally the maximum amount of images allowed. In my case 5 each.

exports.upload = multer(multerOptions).fields([{name:'headshots', maxCount: 5}, {name:'bodyshots', maxCount: 5}]);

In our resize function this is where we are doing the bulk of the work. We are looping through req.files provided by multer to access to image data object. req.files gives us an object of image arrays as shown below:

{
  headshots: [
    {
      fieldname: 'headshots',
      originalname: 'headshot_1.jpg',
      encoding: '7bit',
      mimetype: 'image/jpeg'
    },
    {
      fieldname: 'headshots',
      originalname: 'headshot_2.jpg',
      encoding: '7bit',
      mimetype: 'image/jpeg'
    },
    ...
  ],
  bodyshots: [
    {
      fieldname: 'bodyshots',
      originalname: 'bodyshot_1.jpg',
      encoding: '7bit',
      mimetype: 'image/jpeg'
    },
    {
      fieldname: 'bodyshots',
      originalname: 'bodyshot_2.jpg',
      encoding: '7bit',
      mimetype: 'image/jpeg'
    },
    ...
  ]
};

Here is the tricky part because in this particular situation I need to loop over two sets of multiple images, upload them to Cloudinary and get the returned urls to be placed in req.body.headshots and req.body.bodyshots to then be used in our createModel function, I needed to create a promise helper function and map over these promises and finally resolve with Promise.all().

// Helper function
function uploadToCloudinary(image) {
    return new Promise((resolve, reject) => {
      cloudinary.uploader.upload_stream(
        {
          resource_type: 'image',
          width: 800,
          crop: 'scale',
          quality: 80
        },
        function(error, result){
          if(error) {
            reject(error);
          } else {
            resolve({"url":result.url});
          }
        }
      ).end(image.buffer);
    })

}
let counter = 0;
  for (const fieldname in fileObjects) {
    const images = fileObjects[fieldname];
    let promises = images.map((image) => {
      return uploadToCloudinary(image);
    });

    Promise.all(promises)
      .then(res => {
        counter = counter + 1;
        req.body[fieldname] = res;
        if(counter == 2) {
          next(); // This then calls the createModel after all promises of the for loop are resolved
        }
      })
      .catch(err => {
        console.log(err);
      })

  }

Now that we have our req.body.headshots and req.body.bodyshots parameters filled with the information Airtable needs we can now use Airtable’s api to create a new model record.

I hope this helps anyone who requires this functionality since there is no documentation as far as I have found to accomplish this. Of course the code would be much simpler if you need to just upload one image or if you only had one image field.