Generate PDFs in Express

Add a PDF download to an existing Express page, or generate a PDF from a template. The examples use PDFCrowd's HTML to PDF API and work for invoices, reports, receipts, and other documents built from application data.

Set up the client

Install the Node.js client in your Express application:

npm install pdfcrowd

You can run the examples below with the included demo credentials, or replace them with your PDFCrowd username and API key.

Add a PDF download to an existing page

An existing page can display its usual HTML and return a PDF when the user clicks a download button. Reuse the page's template, data preparation, and access checks.

Choose what to send to PDFCrowd:

Convert rendered HTML

Add this handler where your application registers its document routes, using your existing app. The getDocumentForCurrentUser(req, documentId) function is a placeholder for your asynchronous document lookup and permission checks. Replace it with your own logic; it should reject missing or inaccessible documents. The EJS template at views/documents/detail.ejs receives the result as document.

Keep your authentication and CSRF middleware before these routes. The POST handler also expects express.urlencoded() to have parsed the form body.

const pdfcrowd = require('pdfcrowd');

async function documentDetail(req, res, next) {
    const { documentId } = req.params;
    if (!/^\d+$/.test(documentId)) return res.sendStatus(404);

    const document = await getDocumentForCurrentUser(req, documentId);
    if (req.method !== 'POST' || req.body?.download_pdf !== '1') {
        return res.render('documents/detail', { document });
    }

    res.render('documents/detail', { document }, (error, html) => {
        if (error) return next(error);

        try {
            const client = new pdfcrowd.HtmlToPdfClient('demo', 'demo');
            client.setContentViewportWidth('balanced');
            const chunks = [];
            const callbacks = {
                data: stream => {
                    stream.on('data', chunk => chunks.push(chunk));
                },
                error: message => next(new Error(message)),
                end: () => {
                    if (res.destroyed) return;
                    try {
                        const pdf = Buffer.concat(chunks); // PDF bytes
                        res.attachment(`document-${documentId}.pdf`);
                        res.type('pdf').send(pdf);
                    } catch (error) {
                        next(error);
                    }
                },
            };
            client.convertString(html, callbacks);
        } catch (error) {
            next(error);
        }
    });
}

app.route('/documents/:documentId')
    .get(documentDetail)
    .post(documentDetail);

Add this form to the EJS template, outside any existing form:

<form method="post" class="pdfcrowd-remove">
    <!-- Keep your application's CSRF token field here. -->
    <button type="submit" name="download_pdf" value="1">Download PDF</button>
</form>

Express does not include CSRF protection. Keep your application's token field in the form and its validation middleware on the POST route; the comment above is a placeholder for that field.

GET requests display the HTML page. Submitting the download button sends a POST, renders the same template, converts it, and returns a PDF download. Passing a callback to res.render() obtains the HTML without sending it to the browser. The pdfcrowd-remove class hides the form in the PDF while leaving it visible on the webpage.

The SDK's data callback supplies a readable stream. Its binary chunks are collected in memory, and end sends the complete Buffer as application/pdf. No PDF headers or bytes are sent before conversion succeeds. The SDK calls error on failure instead of end.

Errors reach Express error handling through next(error). Keep production error responses generic and SDK details in server logs; do not use SDK error codes as HTTP response statuses. Set NODE_ENV=production when using Express's default error handler. See SDK error handling for details.

To display the PDF in the browser, replace res.attachment(...) with res.set('Content-Disposition', 'inline'). Set page size, margins, or other PDF options before convertString().

Make CSS and images available

PDFCrowd needs reachable URLs for stylesheets, images, and fonts referenced by the rendered HTML. Absolute asset URLs, such as https://your-site.example/css/document.css, need no additional base URL.

For relative paths, set the public site or directory URL in your application setup:

app.locals.pdfBaseUrl = 'https://your-site.example/';

Replace https://your-site.example/ with your website's public URL. Then add this to the EJS template's <head>, before stylesheet links:

<% if (typeof pdfBaseUrl !== 'undefined' && pdfBaseUrl) { %>
<base href="<%= pdfBaseUrl %>">
<% } %>

Express makes app.locals available to both res.render() and app.render(). The <base> therefore applies whenever this template is rendered, including the normal HTML page. It sets the base for relative assets and hyperlinks: css/document.css becomes https://your-site.example/css/document.css. For assets beneath a directory, use a base such as https://your-site.example/app/, including the trailing slash. Paths starting with / still resolve from the site's root. Avoid adding a second <base> if one already exists.

PDFCrowd cannot reach assets on your development machine through localhost or 127.0.0.1. For local assets, you can instead package the HTML and assets in a ZIP archive. Keep paths relative to the archive's folders and omit the website <base> for that approach.

Convert the page URL instead

Instead of sending rendered HTML, you can ask PDFCrowd to open the page through its URL. First configure your public origin in the application setup, without a path or trailing slash:

app.set('publicOrigin', 'https://your-site.example');

Keep the lookup, POST check, and ordinary HTML response above. In the PDF branch, remove the opening res.render(..., (error, html) => { line, the following if (error) return next(error); line, and the matching closing });. Keep the enclosed try/catch block, including client setup, callbacks, and response code. Within that block, replace the convertString() line with:

const url = app.get('publicOrigin') + req.originalUrl;
client.convertUrl(url, callbacks);

This preserves the current path and query parameters while keeping the origin under your application's control. PDFCrowd makes a separate GET request, so the route returns HTML; only the original POST triggers conversion. The server must be able to handle that GET while the POST waits for the PDF. The page and its assets must be reachable from PDFCrowd.

The separate request does not inherit the user's Express session. For protected pages, configure cookies or HTTP authentication as appropriate, or use rendered HTML from the original request. If your session middleware locks the session until the POST finishes, forwarding that session can block the GET; use rendered HTML instead.

Generate a PDF from a template

You can also generate a PDF directly from a template and application data. Use an existing EJS template or a separate template designed for the document.

This helper uses your configured Express app to render the template and delivers a PDF Buffer to its callback. It uses the same asset configuration as the route example:

const pdfcrowd = require('pdfcrowd');

function renderPdf(app, template, context, callback) {
    app.render(template, context, (error, html) => {
        if (error) return callback(error);

        try {
            const client = new pdfcrowd.HtmlToPdfClient('demo', 'demo');
            client.setContentViewportWidth('balanced');
            const chunks = [];
            client.convertString(html, {
                data: stream => {
                    stream.on('data', chunk => chunks.push(chunk));
                },
                error: message => callback(new Error(message)),
                end: () => {
                    let pdf;
                    try {
                        pdf = Buffer.concat(chunks);
                    } catch (error) {
                        return callback(error);
                    }
                    callback(null, pdf);
                },
            });
        } catch (error) {
            callback(error);
        }
    });
}

For example, given a document, call it from an Express handler:

renderPdf(app, 'documents/pdf', { document }, (error, pdf) => {
    if (error) return next(error);
    res.attachment('document.pdf').type('pdf').send(pdf);
});

Here app is your configured application, and res and next come from the calling handler. Create views/documents/pdf.ejs for the document layout and pass all its data in context. Choose template names and context keys in application code; do not pass req.query or req.body directly as render options.

The helper sends no HTTP response. Its caller can use the buffer for a download, storage, or email, and receives rendering or conversion failures as the callback's first argument. Outside a request, pass the same configured app and use a template that does not depend on request-local values, such as session data or the download form's CSRF token.

For more conversion inputs and PDF settings, see the Node.js examples and API reference.