Use Puppeteer and Nodejs to take screenshots and PDFs — as a Service
Did you ever thought of a simple solution to create screenshots programmatically?

As a developer I wanted to create a simple service which creates dynamic websites and have them easily sharable via screenshots and PDF. I could have created a simple NodeJS application (like described in this article) just to solve the problem at a time but thought: “Why do I not make it a simple service?”.
The basics — a simple NodeJS application spawning a puppeteer controlled headless browser to capture screenshots and images from a website — where already in place, but now I wanted to make it a bit more versatile and consumable.
What is a bit more?
- A simple and straightforward API
- Param validation
- Documentation
- Testing
- Security considerations
- Docker Images
- Simple deployment to e.g. Heroku
The API
I decided to spawn up a simple NodeJS service with ExpressJS. It is very simple and consists of two endpoints: /api/shot and /api/pdf.
I used apidoc to create the documentation inline of my code, once I build the documentation it is available under /docs (preview).
//...
/**
* @api {get} /pdf obtain a pdf of a page
* @apiName TakeScreenshot
* @apiGroup PDF
* @apiVersion 1.0.0
*
* @apiParam {String} url a url to be looked up
* @apiParam {Number} [w] width of the viewport
* @apiParam {Number} [h] height of the viewport
* @apiParam {String} [d] device to use for the viewport - overwrites other v/h parameters
*
* @apiSuccess {File} pdf the generated pdf
* @apiError {Object} Errors returned errors
*/
//...
✓ Documentation
Beside the documentation, I decided to use ajv for param validation as it is blazing fast and well maintained. The schema live in schema.js and are very simple.
const { devices } = require('./util');
const screenshotSchema = {
title: 'Page screenshot',
description: 'Take screenshot of a page',
type: 'object',
properties: {
url: {
type: 'string',
format: 'uri',
pattern: '^(https?|http?)://',
minLength: 1,
maxLength: 255,
},
selector: {
type: 'string',
minLength: 1,
maxLength: 255,
},
},
required: ['url'],
};
const pdfSchema = {
title: 'PDF page print',
description: 'Take pdf of a page',
type: 'object',
properties: {
url: {
type: 'string',
format: 'uri',
pattern: '^(https?|http?)://',
minLength: 1,
maxLength: 255,
},
w: {
type: 'integer',
minimum: 1,
maximum: 12288,
},
h: {
type: 'integer',
minimum: 1,
maximum: 12288,
},
d: {
type: 'string',
enum: Object.keys(devices),
},
},
required: ['url'],
};
module.exports = { screenshotSchema, pdfSchema };
Now, I just need to make sure I validate my request parameters with the schema for each endpoint.
//...
router.get('/shot', async (req, res) => {
const validate = ajv.compile(screenshotSchema);
const result = await validate(req.query);
if (!result) {
const errors = await parseAJVErrors(validate.errors);
// return with the validation errors
return res.status(400).json({ errors });
}
const { url, selector } = req.query;
//use the params now
✓ Param validation
And finally we want to have a simple API — just two endpoints replying to GET requests. One for screenshots available at /api/shot and the second for PDFs available at /api/pdf. As we work with GET requests, we only support query parameters.
✓ Simple API
Testing
It is a key differentiator to not only publish a project as open source and let it be there, but having also a set of tests that others can verify the functionality through running the tests.
I decided to try out ava with this project and I have to admit, that it offers a candid experience to the your main functionality.
const test = require('ava');
const { shot: screenshot, pdf } = require('./capture');
const fs = require('fs');
test('create screenshots', async (t) => {
const screenshotBuffer = await screenshot({ url: 'https://www.google.com' });
t.assert(
screenshotBuffer.toString('binary').length > 1,
'Should have generated a screenshot',
);
});
test('create screenshot by selector', async (t) => {
const screenshotSelectorBuffer = await screenshot({
url: 'https://card-joy.web.app/v?img=c1&t=Merry%20Christmas!&p=topLeft',
selector: '#root > div > div > div > img',
});
t.assert(
screenshotSelectorBuffer.toString('binary').length > 1,
'Should have generated a screenshot',
);
});
test('create pdf', async (t) => {
const generated = await pdf({ url: 'https://www.google.com' });
t.assert(generated.length > 0, 'Should have generated a pdf');
});
And finally I set the test command in the package.json to “test”: “ava — verbose” — using verbose to get more information on each testcase.
✓ Testing
Security
As this is a service, which can easily pulled into an existing project as a single purpose microservice, I decided to use helmet to secure the Express App which sets various HTTP headers to mitigate common security pitfalls.
Last but not least, I added rate-limiting which defaults to 20 requests per 15 minutes per IP and is stored in memory.
✓ common security pitfalls covered
Deployment
The service can easily be run with docker. The image packages all dependencies including a chrome browser.
$ docker run -it --rm -p 3000:3000 chrkaatz/the-snap
Or if you already got a Heroku Account, you can use the Deploy to Heroku Button on the Project.
For the setup, I added app.json with the following contents to use Nodejs and a Buildpack enabling the use of puppeteer within Heroku.
{
"name": "The Snap",
"description": "A simple service to obtain screenshots and PDFs from pages",
"repository": "https://github.com/chrkaatz/the-snap",
"logo": "https://github.com/chrkaatz/the-snap/raw/main/logo.png",
"keywords": ["node", "puppeteer", "screenshot", "api", "pdf"],
"buildpacks": [
{
"url": "heroku/nodejs"
},
{
"url": "https://buildpack-registry.s3.amazonaws.com/buildpacks/jontewks/puppeteer.tgz"
}
],
"env": {
"HUSKY": {
"description": "Disable git hooks managed by husky",
"value": "0"
}
}
}
✓ Deployment
Code
The source can be found at https://github.com/chrkaatz/the-snap and the working version checked out at https://the-snap.herokuapp.com/.
Final note: The Homepage is made with MVP.css and is super simple to use. It only took 5 minutes…