Build a PhantomJS Website Screenshot App (Part 2): The Server

In Part 1, we saw how the page and screen capture API work. We even built a generator that we will use to generate screenshots. In this article, we will be building our server and the backend version of our application.

Project Structure

The project directory is simple, we have a public directory where we store CSS, javascript, HTML, and screenshots. Then we have our resources directory where we put our es6 and sass files. The remaining files are files our application requires to work. You will understand a file's purpose as we go through our application development.

Building the Server

Building the server requires some dependencies, we will use express for handling HTTP requests. We also need morgan for logging, body-parser to parse the request body and serve-favicon for the favicon. We also have valid-url for validating a URL and md5 for generating an md5 hash.

{
  "name": "scotchshot",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "body-parser": "^1.15.2",
    "express": "^4.14.0",
    "md5": "^2.1.0",
    "morgan": "^1.7.0",
    "serve-favicon": "^2.3.0",
    "valid-url": "^1.0.9"
  }
}

From the root of our application, we run npm install. Now that we have our dependencies installed, we can move on to building our server. Open up app.js and add the following.

var http = require('http');
var path = require('path');

// if environment port is not set, default to 3000
var port = process.env.PORT || '3000';

var bodyParser = require('body-parser');
var favicon = require('serve-favicon');
var express = require('express');
var app = express();

// if localhost, pretty-print html output
app.locals.pretty = process.env.PORT ? false : true;
app.set('port', port);

// parse json
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// static directory
app.use(express.static(path.join(__dirname, 'public')));

// load our routes
app.use('/', require('./routes'));

// listen
http.createServer(app).listen(port);

That's our server. We now need to configure our routes. We will only use two routes for our project. We open our routes.js and get started.

var express = require('express');
var path = require('path');
var url = require('valid-url');
var router = express.Router();

/* GET home page. */
router.get('/', function (req, res, next) {
    res.render('index');
});

First, we create our first route which returns the index page. Since we did not tell express the view engine to use, it will use HTML by default. In our public directory, we can create index.html. The next route we want to create is /snap. This route is responsible for communicating with our generator.js that we created in the last part.

router.get('/snap', function (req, res, next) {
    var md5 = require('md5');
    var width = req.query.width,
       website = req.query.url;

    if (!isNaN(width) && url.isUri(website)) {
        // Do stuff here
    } else {
        res.status(422);
        return res.json({ message: 'please make sure the url is valid' });
    }
});

First we check if the width is a number and the url parameter is valid. If any of the parameters fail validation, we return a json object with a message property that tells the user to make sure the parameters are valid. Also we return an http status code of 422 meaning unprocessable entity. If validation is successful, we create an md5 hash of the url, and then create our save path.

var hash = md5(website);
var savePath = path.join(__dirname, 'public', 'screenshots', hash) + '.png';

Our save path is public/screenshots/<md5 hash>.png.

Next thing we want to do is build our PhantomJS command. Remember, the command is supposed to look like this.

phantomjs generator.js <website> <savepath> <width> <height>

To create our command, we can create an array of each part of our command and then join them with a space, like this.

var cmd = ['phantomjs', 'generator.js', website, savePath, width, 1].join(' ');

The final thing we need to do in our routes.js is call the command we built above. To do that, we use the inbuilt child_process module and call its exec method.

var exec = require('child_process').exec;

exec(cmd, function (error) {
    if (error) {
        res.status(422);
        return res.json({ message: 'Something went wrong, try reloading the page' });
    }

    return res.json({ path: '/screenshots/'+ hash +'.png' });
});

The first parameter of the exec method is the command to run, the second is a closure that that runs when the command has completed its execution. In the closure, we return a json response with the path to the screenshot url. But if for some reason a problem occurs, we send a 422 error message. Finally, we export the router object.

module.exports = router;

By now, our routes file should look like this.

var express = require('express');
var path = require('path');
var url = require('valid-url');
var router = express.Router();

/* GET home page. */
router.get('/', function (req, res, next) {
    res.render('index');
});

router.get('/snap', function (req, res, next) {
    var md5 = require('md5');
    var width = req.query.width,
        website = req.query.url;

    if (!isNaN(width) && url.isUri(website)) {
        var hash = md5(website);
        var savePath = path.join(__dirname, 'public', 'screenshots', hash) + '.png';
        var cmd = ['phantomjs', 'generator.js', website, savePath, width, 1].join(' ');
        var exec = require('child_process').exec;

        exec(cmd, function (error) {
            if (error) {
                res.status(422);
                return res.json({ message: 'Something went wrong, try reloading the page' });
            }

            return res.json({ path: '/screenshots/'+ hash +'.png' });
        });
    } else {
        res.status(422);
        return res.json({ message: 'please make sure the URL is valid' });
    }
});

module.exports = router;

Add PhantomJS

Your web host might not allow you to install stuff, so for that reason, we need to add PhantomJS in the project. Go to PhantomJS download page. We will download the binaries for windows and Linux. After we have downloaded the binaries, we create a bin directory and add the binaries there. In routes.js, we create a function pathToPhantom and use it to resolve the current binary depending on the operating system.

function pathToPhantom() {
    var phantom = (/^win/.test(process.platform)) ? 'phantomjs.exe' : 'phantomjs';

    return path.join(__dirname, 'bin', phantom);
}

We can now edit the cmd variable to be.

var cmd = [pathToPhantom(), 'generator.js', website, savePath, width, 1].join(' ');

Conclusion

In the last part of this series, we will build the frontend, and integrate with our API.

Samuel Oloruntoba

Self-proclaimed full-stack web developer and a quasi-academic. I work mostly on the backend (PHP and Node) with a recent enthusiasm for frontend development (React, SVG, HTML5 Canvas).