Blog Post

Serving PDFs with Express.js

Illustration: Serving PDFs with Express.js

This post will cover how to serve PDF files with Express.js, the de facto web framework for Node.js. We’ll show three different ways you can serve files and explain how to use the Content-Disposition header to tell the browser how to handle a file on the client side.

Getting Started

First, scaffold out a simple Express app:

mkdir pdf-express && cd pdf-express

npm init -y
touch app.js

sed -i '' 's/index/app/g' package.json

mkdir public views

npm install express ejs

npm install nodemon -D

Here, you’ve downloaded express and ejs as dependencies. Also, you’ve installed nodemon as a development dependency. nodemon is a utility that will monitor for any changes in your source and automatically restart your server.

Next, update app.js with some basic boilerplate:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));
app.set('view engine', 'ejs');

app.listen(port, () => console.log(`app listening on port ${port}`));

Finally, add a "start" script to package.json:

"scripts": {
+   "start": "nodemon app.js",
    "test": "echo \"Error: no test specified\" && exit 1"

And then test that your app runs successfully:

npm start

If all goes well, you’ll see your “Hello World!” message shown in the browser.

Opening PDFs

Your initial goal is to render a list of PDFs and provide links for opening them using the browser’s default PDF handler.

Example Files

First, you’ll need some PDFs to work with. The following command will download a few example files and place them in the public/pdfs folder in your project:

curl | tar -xz - -C ./public

Index Route

At the index (/) route of your app, you’ll read the files from public/pdfs and show a list of them using a template. Then you’ll update app.js as follows:

const express = require('express');
const ejs = require('ejs');
const path = require('path');
const fs = require('fs');

const app = express();
const dirPath = path.join(__dirname, 'public/pdfs');
const port = 3000;

const files = fs.readdirSync(dirPath).map((name) => {
	return {
		name: path.basename(name, '.pdf'),
		url: `/pdfs/${name}`,

app.set('view engine', 'ejs');

app.get('/', (req, res) => {
	res.render('index', { files });

app.listen(port, () => console.log(`app listening on port ${port}`));

Index Template

Now it’s time to create the views/index.ejs file and update it as follows:

		<title>PDF Express</title>
			body {
				margin: 30px;
				background: Aquamarine;
				font: 30px/1.5 'Futura';
				display: flex;
			header {
				margin-right: 80px;
			ul {
				list-style: none;
				padding-left: 0;
				<img src="/img/logo.svg" width="200" />
				<% => { %>
					<a href="<%= file.url %>"><%= %></a>
				<% }) %>

You can download the logo file here. Place the file under the public directory and create the img folder.


Your files will now be listed on the page.


Clicking any of the links will open a file using your browser’s default PDF handler.


Downloading PDFs

That’s all good, but what if your requirements change and you want to download a PDF when clicking a link, rather than opening it?

Content-Disposition: attachment

You can instruct the browser to download the file using the Content-Disposition HTTP header. Its default value is inline, indicating the file “can be displayed inside the Web page, or as the Web page.” Setting it to attachment instead tells the browser to download the file.


Express provides a shortcut for setting the Content-Disposition header with the res.attachment() method.

Note that you can change the name of the file when it’s downloaded by passing the new file name to res.attachment(). This is useful if your file names are opaque (e.g. a digest) and you want to make them more meaningful to the user. It’s worth mentioning that this can also be achieved on the client side with the download attribute of the <a> element.

	express.static('public', {
		setHeaders: (res, filepath) =>

Setting the Header Manually

res.attachment() is just shorthand for setting the header manually. The code below achieves the same result:

  express.static("public", {
    setHeaders: (res, filepath) =>
        `attachment; filename="pdf-express-${path.basename(filepath)}"`


With your header in place, clicking any of the links will download a file.


Viewing PDFs

Predictably, your requirements have changed again. This time, you’re tasked with showing the PDF inline on your page.

Show Route

Start by adding a new show route in app.js that will first find the file from your list using the :file parameter in the URL, and then pass it to your template as the file variable:

app.get('/:file', (req, res) => {
	const file = files.find((f) => === req.params.file);
	res.render('index', { files, file });

You also need to revert your changes to the Content-Disposition header. You can change the value from attachment to inline, or else remove the header-related code and the response will default back to inline.

Index Template

For this example, reuse the views/index.ejs template and add a conditional check to render the file if it’s present. Typically, you’d want to move this to a dedicated show template and move the shared content to a layout.

You can use a really neat trick combining Content-Disposition: inline with the <embed> tag to prompt the browser to show the file inside the <embed> tag:

   	<% => { %>
      	<a href="<%= %>"><%= %></a>
   	<% }) %>
   	<% if((typeof(file) !== 'undefined')) { %>
      <embed src="<%= file.url %>" width="1000px" height="100%" /> <% } %>


With that, your PDF is rendered using the browser’s PDF handler, but right inside your page!



This post looked at three different ways to provide PDF files via Express:

  1. Serving a file and letting the browser decide how to show it.

  2. Instructing the browser to download the file with the Content-Disposition: attachment header.

  3. Combining Content-Disposition: inline with the <embed> tag to show the file inline on your page.

All three methods can come in handy, depending on your use case. And if you want to offer functionality that’s more advanced than simply downloading or showing PDFs, I’d be remiss if I didn’t mention our awesome PDF SDK, PSPDFKit for Web.

Related Products
Share Post
Free 60-Day Trial Try PSPDFKit in your app today.
Free Trial

Related Articles

Explore more
DESIGN  |  Baseline UI • Web

Part V — Mastering the Baseline UI Theme: An In-Depth Exploration

DESIGN  |  Baseline UI • Web

Part IV — Building Consistency: A Guide to Design Tokens in Baseline UI

DESIGN  |  Baseline UI • Web

Part III — Accessible UI Design: Building Inclusive Digital Experiences