ExpressJS and PDFKit – generate a PDF in memory and send to client for download

In my api router, there is a function called generatePDF which aims to use PDFKit module to generate a PDF file in memory and send to client for download instead of displaying only.

In api.js:

var express = require('express');
var router = express.Router();

const PDFDocument = require('pdfkit');

router.get('/generatePDF', async function(req, res, next) {
    var myDoc = new PDFDocument({bufferPages: true});
    myDoc.pipe(res);
    myDoc.font('Times-Roman')
         .fontSize(12)
         .text(`this is a test text`);
    myDoc.end();
    res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-disposition': 'attachment;filename=test.pdf',
        'Content-Length': 1111
    });
    res.send( myDoc.toString('base64'));
});

module.exports = router;

This does not work. The error message is (node:11444) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client.

How can I go about fixing the issue and getting it work?

Also, a relevant question would be how I can separate the business logic of PDF generation from the router and chain them up?

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

Complete solution.

var express = require('express');
var router = express.Router();

const PDFDocument =  require('pdfkit');

router.get('/generatePDF', async function(req, res, next) {
var myDoc = new PDFDocument({bufferPages: true});

let buffers = [];
myDoc.on('data', buffers.push.bind(buffers));
myDoc.on('end', () => {

    let pdfData = Buffer.concat(buffers);
    res.writeHead(200, {
    'Content-Length': Buffer.byteLength(pdfData),
    'Content-Type': 'application/pdf',
    'Content-disposition': 'attachment;filename=test.pdf',})
    .end(pdfData);

});

myDoc.font('Times-Roman')
     .fontSize(12)
     .text(`this is a test text`);
myDoc.end();
});

module.exports = router;

Solution 2

First I recommend to create a service for the PDF kit. And then a Controller to the route that you want.

I used get-stream to make this easier.

It also answers your question to the accepted answer:

how I can separate the business logic of PDF generation from the
router and chain them up?

This is my professional solution:

import PDFDocument from 'pdfkit';
import getStream from 'get-stream';
import fs from 'fs';


export default class PdfKitService {
  /**
   * Generate a PDF of the letter
   *
   * @returns {Buffer}
   */
  async generatePdf() {
    try {
      const doc = new PDFDocument();

      doc.fontSize(25).text('Some text with an embedded font!', 100, 100);

      if (process.env.NODE_ENV === 'development') {
        doc.pipe(fs.createWriteStream(`${__dirname}/../file.pdf`));
      }

      doc.end();

      const pdfStream = await getStream.buffer(doc);

      return pdfStream;
    } catch (error) {
      return null;
    }
  }
}

And then the method of the Controller:

(...) 

  async show(req, res) {
    const pdfKitService = new PdfKitService();
    const pdfStream = await pdfKitService.generatePdf();

    res
      .writeHead(200, {
        'Content-Length': Buffer.byteLength(pdfStream),
        'Content-Type': 'application/pdf',
        'Content-disposition': 'attachment;filename=test.pdf',
      })
      .end(pdfStream);

 
  }

And finally the route:

routes.get('/pdf', FileController.show);

Solution 3

For those how don’t want to waste RAM on buffering PDFs and send chunks right away to the client:

    const filename = `Receipt_${invoice.number}.pdf`;
    const doc = new PDFDocument({ bufferPages: true });
    const stream = res.writeHead(200, {
      'Content-Type': 'application/pdf',
      'Content-disposition': `attachment;filename=${filename}.pdf`,
    });
    doc.on('data', (chunk) => stream.write(chunk));
    doc.on('end', () => stream.end());

    doc.font('Times-Roman')
      .fontSize(12)
      .text(`this is a test text`);
    doc.end();

Solution 4

You can use blob stream like this.

reference: https://pdfkit.org/index.html

const PDFDocument = require('pdfkit');

const blobStream  = require('blob-stream');

// create a document the same way as above

const doc = new PDFDocument;

// pipe the document to a blob

const stream = doc.pipe(blobStream());

// add your content to the document here, as usual

doc.font('fonts/PalatinoBold.ttf')
   .fontSize(25)
   .text('Some text with an embedded font!', 100, 100);

// get a blob when you're done
doc.end();
stream.on('finish', function() {
  // get a blob you can do whatever you like with
  const blob = stream.toBlob('application/pdf');

  // or get a blob URL for display in the browser
  const url = stream.toBlobURL('application/pdf');
  iframe.src = url;
});

pipe all your pdf data to your blob and then write it to a file or url.
or u can store the pdf directly into cloud storage like firebase storage and send download link to client.

If you want to generate pdfs dynamically then you can also try out html-pdf library in node which allows you to create a pdf from html template and add dynamic data in it. Also it is more reliable than pdfkit
https://www.npmjs.com/package/html-pdf
Also refer this link
Generate pdf file using pdfkit and send it to browser in nodejs-expressjs

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply