Generating PDFs with Node, PDFkit, and Serverless on AWS Lambda
Tags: javascript, serverless, aws, lambda
Published: 2019-11-04 05:05:32

---
title: Generating PDFs with Node, PDFkit, and Serverless on AWS Lambda
published: true
date: 2019-11-02 16:56:08 UTC
tags: JavaScript,Serverless,aws,lambda
canonical_url: https://stegosource.com/generating-pdfs-node-pdfkit-serverless-aws-lambda/
cover_image: https://stegosource.com/wp-content/uploads/serverless-blog-cover.svg
---

There are a few blog posts out there already covering this subject, but most of them include extra packages or steps than are actually necessary. In this post, I’ll cover only the minimum to create a Serverless function on AWS Lambda which generates PDFs using Node and PDFKit. No need for Express, no HTML parsing, and no uploading to S3.

## Setting up AWS Lambda with Serverless

Getting started with [serverless](https://serverless.com%EF%BF%BD) functions with [Node](https://nodejs.org/en/) on [AWS Lambda](https://aws.amazon.com/lambda/) is pretty straight forward. We will need to use a configuration file called `serverless.yml` (for more details on the file options, [see here](https://serverless.com/framework/docs/providers/aws/guide/serverless.yml/)). This file should look something like so:

```
service: generatePdf

provider:
  name: aws
  region: us-west-1
  runtime: nodejs10.x

functions: 
  generatePdf:
    handler: src/index.generatePdf
    events:
      - http:
          path: /pdf
          method: get
```

This configuration assumes we have a function called `generatePdf` which is exported from the file called `index.js` located inside a folder called `src`.

Each serverless function handler is provided with 3 parameters:

1. The request “`event`” containing all sorts of details like the route requested, the request method, the request headers, and more.
2. The lambda “`context`” which provides details about the context of the function, as well as some methods for the response.
3. A Node.js style, [error-first](http://thenodeway.io/posts/understanding-error-first-callbacks/) “`callback`” function used to send back the response data.  

Here is a very basic handler example. Note that the callback function expects an object for the response ( **not** a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object), which must have a “`body`” key:

```
exports.generatePdf = (event, context, callback) => {
  console.log('details about the event: /n, event)
  console.log('details about the context: /n, event)

  const response = {
    body: "hello world"
  }
  callback(false, response)
}
```

If you’re like me and prefer promises, we can convert it to use [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) like so:

```
exports.generatePdf = async (event, context) => {
  console.log('details about the event: /n, event)
  console.log('details about the context: /n, event)

  const response = {
    body: "hello world"
  }
  
  return response
}
```

Cool. Our basic lambda function is working.

## Generating PDFs in Node with PDFKit

Next, we will look at generating a PDF in Node. There are a few options, but the one I found most common was [PDFKit](https://pdfkit.org/). You can install it in your project with “`npm install pdfkit`“.

A basic “hello world” example for generating a PDF **in memory** requires us to use buffers. It looks something like this:

```
const PDFDocument = require("pdfkit")

const doc = new PDFDocument()

doc.text('hello world', 100, 50)

doc.end()

const buffers = []
doc.on("data", buffers.push.bind(buffers))
doc.on("end", () => {
    const pdfData = Buffer.concat(buffers)
    console.log(pdfData)
  })
})
```

This is fine, but since we are using async/await, we want to use a Promise instead of a callback:

```
const PDFDocument = require("pdfkit")

const pdfPromise = new Promise(resolve => {
  const doc = new PDFDocument()

  doc.text('hello world', 100, 50)
  doc.end()

  const buffers = []
  doc.on("data", buffers.push.bind(buffers))
  doc.on("end", () => {
    const pdfData = Buffer.concat(buffers)
    resolve(pdfData)
  })
})
```

## Adding PDFs as a serverless response

We’re almost done. We have a serverless endpoint that runs a Node function, and we have a Node function that generates a PDF. However, there is a little bit of configuration needed in the AWS Gateway in order to server PDFs to the browser as binary data.

First, we need to install the Serverless plugins [serverless-apigw-binary](https://github.com/maciejtreder/serverless-apigw-binary) and [serverless-apigwy-binary](https://github.com/ryanmurakami/serverless-apigwy-binary) (it’s not a typo, they are close, but not the same). We can do so with `npm install serverless-apigw-binary serverless-apigwy-binary`.

With those installed, we also need to make a few changes to our `serverless.yml` file. We need to tell the AWS Gateway to include binary media types, tell our `generatePdf` function to serve the content as binary, include the aforementioned plugins, and tell AWS which content type to serve as binary based on the HTTP header it receives:

```
service: generatePdf

provider:
  name: aws
  region: us-west-1
  runtime: nodejs10.x
  # This is new
  apiGateway:
    binaryMediaTypes:
      - "*/*"

functions: 
  generatePdf:
    handler: src/index.generatePdf
    events:
      - http:
          path: /pdf
          method: get
          # This is new
          contentHandling: CONVERT_TO_BINARY

# This is new
plugins:
  - serverless-apigw-binary
  - serverless-apigwy-binary

# This is new
custom:
  apigwBinary:
    types:
      - "application/pdf"

```

With that in place, we can edit our previous “hello world” serverless function to use the PDFKit generation. We also have to make sure to `base64` encode our PDF buffer, send the appropriate “application/pdf” `content-type` response header, and set the `isBase64Encoded` flag for the response to `true`:

```
const PDFDocument = require("pdfkit")

exports.generatePdf = async () => {
  const pdfBuffer = await new Promise(resolve => {
    const doc = new PDFDocument()

    doc.text('hello world', 100, 50)
    doc.end()

    const buffers = []
    doc.on("data", buffers.push.bind(buffers))
    doc.on("end", () => {
      const pdfData = Buffer.concat(buffers)
      resolve(pdfData)
    })
  })

  return {
    headers: {
      "content-type": "application/pdf",
    },
    body: pdfBuffer.toString("base64"),
    isBase64Encoded: true,
  }
}
```