History: Photo sharing service

Revision made 3 years ago by Francisco Presencia. Go to the last revision.

We are going to create a photo sharing service where the user is able to upload pictures and like them.

We will be using a simple MVC, so our file structure will be:

/controllers
  /photos.js
/models
  /photos.js
/views
  /index.pug
  /layout.pug
/public
  /images
    /1.jpg
    /2.jpg
    ...
  /style.css
/.env
/.gitignore
/app.js
/package.json
/Procfile
/routes.js

So far there will be no user auth as it'd take too long to explain it all. We will need to install few packages within our web folder, which will be explained as we go:

npm install auto-load body-parser cloudinary dotenv express formidable mongoose pug --save

A small reminder about requests flow; when the user requests any page in any way, it will reach the app.js. In that file we will have all of our server initial setup, and then the flow will continue with our router. From the router we specify what call goes where.

So we first set up our app.js:

// Load the environment configuration from ".env"
require('dotenv').config();

// Import the modules that we will use in this file
var express = require('express');
var mongoose = require('mongoose');
var routes = require('./routes');

// Server configuration
var app = express();
app.set('view engine', 'pug');  // The "html engine"
app.use(express.static('public'));  // Where the static files are

// Connect to the database before accepting any request
mongoose.connect('mongodb://localhost/test');
var db = mongoose.connection;
db.once('open', function(){

  // Use the routes stored in ./routes.js
  app.use(routes);
 
  // Actually start listening to the requests
  app.listen(process.env.PORT || 3000);
});

Then we set up the basic routes. We will be handling requesting the main page with all of the images and uploading a new image with some extra info. In routes.js:

var express = require('express');
var router = express.Router();
var controllers = require('auto-load')('controllers');

router.get('/', controllers.photos.index);

router.post('/photos', controllers.photos.upload);

module.exports = router;

Finally, we will write the main logic for each of our calls in controllers/photos.js. If we had more logic, then we would create different controllers and routes to split it all properly.

// Mock up the data for the index
module.exports.index = function(req, res){
  res.render('index', {
    photos: ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg']
  });
}

// Redirect to home after POSTing to /photos
module.exports.upload = function(req, res, next){
  res.redirect('/');
}

Our layout.pug:

doctype html
html
  head
    title= title
    link(rel="stylesheet" href="https://cdn.jsdelivr.net/picnicss/6.0.0/plugins.min.css")
    link(rel='stylesheet', href='style.css')
    block head
  body
    block content

    script.
      document.addEventListener("DOMContentLoaded", function() {
        [].forEach.call(document.querySelectorAll('.dropimage'), function(img){
          img.onchange = function(e){
            var inputfile = this, reader = new FileReader();
            reader.onloadend = function(){
              inputfile.style['background-image'] = 'url('+reader.result+')';
            }
            reader.readAsDataURL(e.target.files[0]);
          }
        });
      });

In index.pug:

extends layout

block content

  h1= name

  div.flex.one.two-500.five-900
    each photo in photos
      div.photo
        img(src=photo)
        button.like Like

In our style.css:

body {
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

img {
  width: 100%;
}

So far it displays a list with the images decently formatted. You can add any more styles and parts of the website that you see fit.

Uploading the pictures

To upload the pictures we need to add some logic to handle it. Heroku and other Node.js hosting platforms don't have a filesystem where you can store the user-uploaded pictures. So we will use Cloudinary to store our images and try to leave this logic in only one place so we can switch vendors easily.

But first things first; let's add a form to our website. We add this to the end of our index.pug:

form(action="/photos" method="POST" enctype="multipart/form-data")
  div.flex.two
    div
      label.dropimage
        input(type="file" name="image" title="Drop image or click me")
    div
      label Title:
        input(name="title" placeholder="Title")
      label Nickname:
        input(name="nick" placeholder="Nickname")
      input(type="submit" value="Upload")

The important bits are:

  • Remember to use the method=POST
  • The enctype="multipart/form-data" to say that there will be files posted
  • Use an <input type="file"> to upload the pictures.

Now let's create an account in Cloudinary and put our values in .env:

cloud=xxx
key=yyy
secret=zzz

Our controllers/photos.js then uses that configuration with the official library to upload the files. We will also use formidable to handle the posting of the images in the form:

avar formidable = require('formidable');
var cloudinary = require('cloudinary');

// Use our configuration in .env
cloudinary.config({
  cloud_name: process.env.cloud,
  api_key: process.env.key,
  api_secret: process.env.secret
});

module.exports.index = function(req, res){
  models.photos.find({}, function(err, images){
    res.render('index', { photos: images });
  });
}

module.exports.upload = function(req, res, next){
    // Handle the form with an image  var form = new formidable.IncomingForm();
  form.parse(req, function (err, fields, files) {
    if (err) next(err);//    cloudinary.uploader.upload(files.image.path, function (result) {
      // Let's force https in the image:
      var url = result.url.replace('http://', 'https://');

      redirect('/');
    });
  });
}

The model.js:

var mongoose = require('mongoose');

var photosSchema = mongoose.Schema({
  title: { type: String, required: true },
  nickname: { type: String, unique: true, required: true },
  url: { type: String, required: true },
  likes: [{ type: String }]
});

module.exports = mongoose.model('Photos', photosSchema);