Image uploading with React.js, Node.js, and AWS S3

In the past I've blogged about Image uploading and processing with Node.js and GraphicsMagick. In this post I will walk you through how to upload images with React.js, and upload them to AWS S3 with Node.js and some helpful libaries. This post assumes working knowledge of React.js, Express.js, Node.js, and AWS S3. I'll be using ES6 in client-side React.js code.

A few notes:

  • In a production app we would separate the XHR calls using libraries like Flux, and better handle XHR errors, but I'll be excluding those details here in order to keep things concise and focused on the topic at hand.
  • I'll be using modern technologies, such as The File Reader Object for use with modern browsers, and won't get into cross browser compatibility.

Upload the image with our React.js Component

Below is our React.js code. Here we are using the File Reader Object to read the contents of the image file, and will later convert it to a Buffer which will allow us to not worry about saving any files locally.

import React, {Component} from 'react';
import {bindAll} from 'lodash';
import $ from 'jquery';

class ImageUploader extends Component {

  constructor() {
    this.state = {
      data_uri: null,
      processing: false
    }

    bindAll(this, 'handleFile', 'handleSubmit');
  }

  handleSubmit(e) {
    e.preventDefault();
    const _this = this;

    this.setState({
      processing: true
    });

    const promise = $.ajax({
      url: '/api/v1/image',
      type: "POST",
      data: {
        data_uri: this.state.data_uri,
        filename: this.state.filename,
        filetype: this.state.filetype
      },
      dataType: 'json'
    });

    promise.done(function(data){
      _this.setState({
        processing: false,
        uploaded_uri: data.uri
      });
    });
  }

  handleFile(e) {
    const reader = new FileReader();
    const file = e.target.files[0];

    reader.onload = (upload) => {
      this.setState({
        data_uri: upload.target.result,
        filename: file.name,
        filetype: file.type
      });
    };

    reader.readAsDataURL(file);
  }

  render() {
    let processing;
    let uploaded;

    if (this.state.uploaded_uri) {
      uploaded = (
        <div>
          <h4>Image uploaded!</h4>
          <img className='image-preview' src={this.state.uploaded_uri} />
          <pre className='image-link-box'>{this.state.uploaded_uri}</pre>
        </div>
      );
    }

    if (this.state.processing) {
      processing = "Processing image, hang tight";
    }

    return (
      <div className='row'>
        <div className='col-sm-12'>
          <label>Upload an image</label>
          <form onSubmit={this.handleSubmit} encType="multipart/form-data">
            <input type="file" onChange={this.handleFile} />
            <input disabled={this.state.processing} className='btn btn-primary' type="submit" value="Upload" />
            {processing}
          </form>
          {uploaded}
        </div>
      </div>
    );
  }
}

export default ImageUploader;

Handle the API Request

Next up our Node.js API. Here we are using Express.js, and another module named ImageUploader, that we will define in a bit which will handle the image uploading.

First we will handle the request:

var express = require('express');
var app = express();
var ImageUploader = require('../utils/imageUploader');

app.post('/api/v1/image', function (req, res) {

  var image = ImageUploader({
    data_uri: req.body.data_uri,
    filename: req.body.filename,
    filetype: req.body.filetype
  }).then(onGoodImageProcess, onBadImageProcess);

  function onGoodImageProcess(resp) {
    res.send({
      status: 'success',
      uri: resp
    });
  }

  function onBadImageProcess(resp) {
    res.send({
     status: 'error'
    });
  }

});

Upload to S3

Next in our imageUploader module we will handle uploading to S3. We are going to use a couple tools here:

  • Q for promises
  • knox to help us upload to S3

Because we are working with base64 data, we will instantiate a buffer.

var Q = require('q');
var knox = require('knox');

var ImageUploader = function(options){

  var deferred = Q.defer();
  var buf = new Buffer(options.data_uri.replace(/^data:image\/\w+;base64,/, ""),'base64');

  knoxClient = knox.createClient({
    key: 'YOUR_S3_KEY,
    secret: 'YOUR_SE_SECRET,
    bucket: 'YOUR_BUCKET_NAME'
  });

  // put to a path in our bucket, and make readable by the public
  req = knoxClient.put('/images/' + options.filename, {
   'Content-Length': buf.length,
   'Content-Type': options.filetype,
   'x-amz-acl': 'public-read' 
  });

  req.on('response', function(res) {
    if (res.statusCode === 200) {
      deferred.resolve(req.url);
    } else
      deferred.reject({error: 'true'});
  });

  req.end(buf);
  return deferred.promise;
}

module.exports = ImageUploader;

Once our promise resolves (hopefully!), we will res.send our data back to our React component, and display the uploaded image.

Questions, comments, improvements?