This blog post was written under the Pusher Guest Writer program.

When your app offers social sharing features, realtime notifications could be vital.

One example is Instagram, a popular app for editing and sharing photos and videos with friends & family. Users get realtime updates as feeds on posts made by their friends and other people they follow on the the platform.

In this tutorial, we will implement a similar social sharing app using some common developer tools, such as:

  • Vue: Frontend framework to simplify our DOM interactions.
  • Node: JavaScript server for handling requests from clients, as well as sending responses
  • Pusher: Free real time pub/sub service. Pusher makes real time as easy as using basic events.
  • Cloudinary: End-to-end image management solution that enables storage, manipulation, optimization and delivery.

Of course, there are other utility tools, like Bootstrap, Express and NeDB, that simplify some time-consuming tasks. We will learn about those while we walk through the demo.

Let’s first build a server for the app.

Setting Up a Node Server

A simple Node server is enough for the task at hand. To create one, run the following init command in an empty directory:

npm init -y

You should see a package.json file right in the folder. You can start installing the dependencies needed in the project:

npm install --save express nedb cors body-parser connect-multiparty pusher cloudinary

The dependencies help with the following outlined tasks:

  • express: Routing framework for Node
  • nedb: Disk database. This is not recommended for a large project, but is good enough to persist data in our demo.
  • cors: Express middleware to enable CORS.
  • body-parser: Express middleware that parses the request body and attaches to the express request object.
  • connect-multiparty: Just like body-parser, but parses uploaded files
  • pusher: Pusher SDK
  • cloudinary: Cloudinary SDK

Next step is to import these installed dependencies into the entry JavaScript file. Create a file named index.js at the root of the directory and start importing the dependencies:

    // Import dependecies
    const express = require('express');
    const multipart = require('connect-multiparty');
    const bodyParser = require('body-parser')
    const cloudinary = require('cloudinary');
    const cors = require('cors');
    const Datastore = require('nedb');
    const Pusher = require('pusher');

    // Create an express app
    const app = express();
    // Create a database
    const db = new Datastore()

    // Configure middlewares
    app.use(cors());
    app.use(bodyParser.urlencoded({ extended: false }))
    app.use(bodyParser.json())

    // Setup multiparty
    const multipartMiddleware = multipart();

Not only have we imported the dependencies, we also configured the Express middleware that was installed.

Configurations
We need to configure Pusher and Cloudinary before actually making use of them. Configuration involves telling the SDKs who or what server should it talk to. This is done by passing it a config object that contains the credentials you retrieve after creating an account. (To learn how to set up both accounts, refer to Appendix 1 and 2 at the end of the article.)

    // Pusher configuration
    const pusher = new Pusher({
      appId: process.env.PUSHER_APPID,
      key: process.env.PUSHER_KEY,
      secret: process.env.PUSHER_SECRET,
      encrypted: true,
      cluster: process.env.PUSHER_CLUSTER
    });

    // Cloudinary configuration
    cloudinary.config({
        cloud_name: process.env.CL_CLOUD_NAME,
        api_key: process.env.CL_KEY,
        api_secret: process.env.CL_SECRET
    });

It’s bad practice to hard code credentials in your code, hence we have added them using environmental variables.

Routes
Two routes are needed for the application — one to serve all the gallery images and another to create new gallery images from a request. Here is one for listing images:

    app.get('/', (req, res) => {
      db.find({}, function (err, docs) {
        if(err) {
          return res.status(500).send(err);
        }
        res.json(docs)
      });
    })

This looks for all the items in our database, and if no error was encountered, sends them as a JSON response to the requesting client. If an error was encountered, the error will be sent as a server error (500).

Let’s now see how the images are uploaded, how data is persisted, and how Pusher emits a real time event that a new image was added to the collection:

    app.post('/upload', multipartMiddleware, function(req, res) {
      // Upload image
      cloudinary.v2.uploader.upload(
        req.files.image.path,
        { /* Transformation if needed */ },
        function(error, result) {
          if(error) {
            return res.status(500).send(error)
          }
          // Save record
          db.insert(Object.assign({}, result, req.body), (err, newDoc) => {
            if(err) {
              return res.status(500).send(err);
            }
            // Emit realtime event
            pusher.trigger('gallery', 'upload', newDoc);
            res.status(200).json(newDoc)
          })
      })
    });  

What’s going on will be better explained as points, so let’s do that:

  • The middleware, multipartMiddleware, was not included in all the routes with use. Rather, it was added to the only route that needs it, which is the above POST /upload route.
  • We use Cloudinary’s upload() method to send the image received to your server. It takes the path to the image being uploaded, a transformation object and the callback function.
  • If the upload was successful, we store the image upload response alongside the request body in our database.
  • After storing the new data, we emit a Pusher upload event on the gallery channel. This event has a payload of the newly created item. All subscriptions to this channel’s event will be notified when an image is successfully uploaded.

Listen and Run
Finally, in the server, we can bind to a port:

    app.listen(process.env.PORT || 5000, () => console.log('Running...'))

This uses the port provided in the environmental variable. If none, it sticks to port 5000.

You can start running the server with:

    node index.js

Setting Up Vue

Vue is the framework that powers our client app. With a server running, we can now implement a client that communicates with this server via HTTP requests and Pusher events.

Start with initializing a Vue project using the Vue CLI:

    ## Install Vue CLI
    npm install -g vue-cli

    ## Scafold a project. Syntax: vue init <template> <name>
    vue init webpack-simple gallery-client

Next, install dependencies:

    npm install --save axios vodal pusher-js cloudinary-core
  • axios: This is a HTTP library that simplifies how we make Ajax requests by enabling us to use promises to handle async.
  • vodal: Vue widget for dialog boxes
  • pusher-js: Pusher client SDK
  • cloudinary-core: Cloudinary client SDK

Gallery Items List

We need to display a list of existing images in the gallery at start up. Therefore, when the app is launched, the user should be presented with a list of all the images available. To achieve this, in the App.vue (the entry component) created lifecycle method, make a request for all the images using axios:

    <script>
    // ./App.vue
    import axios from 'axios';
    import cloudinary from 'cloudinary-core'

    export default {
      name: 'app',
      data () {
        return {
          images: [],
          cl: null,
          spin: false
        }
      },
      created() {
        this.spin = true
        this.cl = new cloudinary.Cloudinary({cloud_name: '<CLOUD_NAME>', secure: true})
        axios.get('http://localhost:5000')
          .then(({data}) => {
            this.spin = false
            this.images = data.map(image => {
              image.url = this.cl.url(image.public_id, {width: 500, height: 400, crop: "fill"})
              return image;
            });
          })
      },
      methods: {
        // Coming soon
      }
    }
    </script>

When the images are fetched, we transform them by manipulating the dimensions (width and height) to fit our design idea. The transformed data is then bound to the view by setting it as the value of the images property :

    <template>
     <!-- ./App.vue -->
      <div id="app">
        <div class="container">
          <h3 class="text-center">Realtime Gallery <button class="btn btn-info" @click="showModal"><span class="glyphicon glyphicon-upload"></span></button></h3>
          <gallery-list :images="images"></gallery-list>
        </div>
        <span v-show="spin" class="glyphicon glyphicon-repeat fast-right-spinner"></span>
      </div>
    </template>

There is also a spin boolean property that determines if a loading spinner should be shown or not. Soon, we will implement the showModal method that is called when the upload button is clicked.

Rather than having native elements all over in the App‘s template, we have a created an abstraction. The gallery-list element is used and is passed the list of images. For it to work, you need to create, import and declare the GalleryList component in App.

First, import and declare it:

    <script>
    // ./App.vue
    import GalleryList from './GalleryList.vue'
    //...
    export default {
      components: {
        'gallery-list': GalleryList
      }
    }
    </script>

Then create the component:

    <!-- ./GalleryList.vue -->
    <template>
      <div>
        <div class="row" v-for="i in Math.ceil(images.length / 3)" :key="i">
          <div class="col-md-4" v-for="image in images.slice((i - 1) * 3, i * 3)" :key="image._id">
            <gallery-item :image="image">
            </gallery-item>
          </div>
        </div>
      </div>
    </template>
    <script>
    import GalleryItem from './GalleryItem.vue'
    export default {
      props: ['images'],
      components: {
        'gallery-item': GalleryItem
      }
    }
    </script>

The component receives images sent from the parent App component via props. We then iterate over the images and display each of them with another component called gallery-item:

    <template>
      <div class="card">
        <h4 class="card-title">{{image.title}}</h4>
        <div class="card-image">
          <img :src="image.url" class="img-responsive"/>
        </div> 
        <p class="card-description">{{image.description}}</p> 
      </div>
    </template>
    <script>
    export default {
      props: ['image']
    }
    </script>

With existing images (which I assume you don’t have yet), you should see the following at localhost:8080 when you run the app with npm run dev:

Gallery Image Upload

Now that we have a list of images to show, the next question is how they came to exist. We have to upload them to the server. Fortunately, the server already made provision for that so all we have left to do is implement the upload logic.

Let’s start by creating an Upload component which contains a form for the upload:

    <template>
      <div class="upload">
        <form @submit.prevent="handleSubmit" enctype="multipart/form-data">
          <div class="form-group">
            <label>Title</label>
            <input type="text" class="form-control" v-model="model.title" />
          </div>
          <div class="form-group">
            <label>Image</label>
            <input type="file" class="form-control" @change="handleUpload($event.target.files)" />
          </div>
          <div class="form-group">
            <label>Description</label>
            <textarea row="5" class="form-control" v-model="model.description"></textarea>
          </div>
          <div class="form-group">
            <button class="btn btn-info">Submit</button>
          </div>
        </form>
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          model: {
            title: '',
            description: '',
            imageFile: null
          }
        }
      },
      methods: {
        handleSubmit() {
          this.$emit('submit', this.model)
        },
        handleUpload(files) {
          this.model.imageFile = files[0];
        }
      }
    }
    </script>

The form contains a title (text), description (text area) and file inputs. These controls are tracked by the model property, which is updated when the values change. title and description are automatic but imageFile is not because it’s read only. Therefore, we have to manually update the model by calling handleUpload every time the file control value changes.

When the form is submitted, we need to call handleSubmit, which triggers an event that will be handled in the parent component (App). Let’s have a look how App handles this:

    <template>
      <div id="app">
        <div class="container">
          <h3 class="text-center">Realtime Gallery <button class="btn btn-info" @click="showModal"><span class="glyphicon glyphicon-upload"></span></button></h3>
          <vodal :show="show" animation="zoom" @hide="show = false">
            <upload @submit="handleSubmit"></upload>
          </vodal>
          <gallery-list :images="images"></gallery-list>
        </div>
        <span v-show="spin" class="glyphicon glyphicon-repeat fast-right-spinner"></span>
      </div>
    </template>
    <script>
    import Upload from './Upload.vue'
    import GalleryList from './GalleryList.vue'
    import axios from 'axios';
    import cloudinary from 'cloudinary-core'
    var Pusher = require('pusher-js');
    export default {
      name: 'app',
      data () {
        return {
          images: [],
          show: false,
          cl: null,
          spin: false
        }
      },
      created() {
        // truncated
      },
      methods: {
        showModal() {
          this.show = true
        },
        handleSubmit(model) {
          this.show = false;
          this.spin = true
          const formData = new FormData()
          formData.append('image', model.imageFile);
          formData.append('title', model.title);
          formData.append('description', model.description);

          axios.post('http://localhost:5000/upload', formData)
          .then(({data}) => {
            this.spin = false
          })
        }
      },
      components: {
        'gallery-list': GalleryList,
        'upload': Upload
      }
    }
    </script>

Because of the way we added GalleryList, the Upload container is imported and included in the list of components. The dialog plugin, vodal is used to only show the form as a dialog when the upload button beside the header is clicked. This is possible by toggling show.

Notice how the upload component handles the emitted submit event:

    <upload @submit="handleSubmit"></upload>

It calls handleSubmit on the containing (parent) component, which uploads the image with axios, hides the model and uses a loading spinner to tell us the status of the upload.

The vodal plugin needs to be imported and configured for it to work. You can do this in the ./main.js file:

    import Vodal from 'vodal';
    Vue.component(Vodal.name, Vodal);

Now you can run the app again (if you stopped it), and try to upload an image:

When you upload an image, you won’t see any UI updates unless you refresh the browser. Let’s implement realtime updates to make UI updates happen.

Realtime Updates

We already have the upload feature fleshed out but we need to let the users know their upload was successful. The server is already triggering an event, all we need do is listen to this event and prepend incoming payload to the existing list of images:

    <script>
    import Upload from './Upload.vue'
    import GalleryList from './GalleryList.vue'
    import axios from 'axios';
    import cloudinary from 'cloudinary-core'
    var Pusher = require('pusher-js');
    export default {
      name: 'app',
      data () {
        return {
          images: [],
          show: false,
          cl: null,
          spin: false
        }
      },
      created() {
        this.spin = true;
        var pusher = new Pusher('<APP_ID>', {
          encrypted: true,
          cluster: 'CLUSTER'
        });
        var channel = pusher.subscribe('gallery');
        channel.bind('upload', (data) => {
          data.url = this.cl.url(data.public_id, {width: 500, height: 400, crop: "fill"})
          this.images.unshift(data)
        });
        // Truncated...
      },
      methods: {
        // Truncated...
      },
    }
    </script>

We bind to the upload event, which we created on the gallery channel, then add the new image that comes into the existing array of images. You can now see image upload and UI updates happen in real time:

Conclusion

At this point, if you have followed the article, you can stop wondering how realtime image sharing apps work and start building one for yourself. If you get stuck, feel free to leave comments or refer to the Demo in the Github repo.

Appendix 1: Pusher Setup

  • Sign up for a free Pusher account

  • Create a new app by selecting Apps on the sidebar and clicking Create New button on the bottom of the sidebar:

  • Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with for a better setup experience:

  • You can retrieve your keys from the App Keys tab

Appendix 2: Cloudinary Setup

  • Sign up on Cloudinary for a free account:

  • When you sign up successfully, you’re presented with a dashboard that holds your cloud credentials. You can safely store them for future use:

About Chris Nwamba

Chris is a JavaScript preacher. He also strives to make something out of other languages. Tech Writer. Dev Evangelist. Speaker.