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 https://pspdfkit.com/images/blog/2020/serving-pdfs-with-expressjs/pdfs.zip | 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.use(express.static('public')); 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:
<html> <head> <title>PDF Express</title> <style> body { margin: 30px; background: Aquamarine; font: 30px/1.5 'Futura'; display: flex; } header { margin-right: 80px; } ul { list-style: none; padding-left: 0; } </style> </head> <body> <header> <h1> <img src="/img/logo.svg" width="200" /> </h1> <ul> <% files.map(file => { %> <li> <a href="<%= file.url %>"><%= file.name %></a> </li> <% }) %> </ul> </header> </body> </html>
You can download the logo file here. Place the file under the public
directory and create the img
folder.
Result
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.
res.attachment()
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.
app.use( express.static('public', { setHeaders: (res, filepath) => res.attachment(`pdf-express-${path.basename(filepath)}`), }), );
Setting the Header Manually
res.attachment()
is just shorthand for setting the header manually. The code below achieves the same result:
app.use( express.static("public", { setHeaders: (res, filepath) => res.set( "Content-Disposition", `attachment; filename="pdf-express-${path.basename(filepath)}"` ); }) );
Result
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) => f.name === 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:
<ul> <% files.map(file => { %> <li> <a href="<%= file.name %>"><%= file.name %></a> </li> <% }) %> </ul> </header> <section> <% if((typeof(file) !== 'undefined')) { %> <embed src="<%= file.url %>" width="1000px" height="100%" /> <% } %> </section>
Result
With that, your PDF is rendered using the browser’s PDF handler, but right inside your page!
Conclusion
This post looked at three different ways to provide PDF files via Express:
-
Serving a file and letting the browser decide how to show it.
-
Instructing the browser to download the file with the
Content-Disposition: attachment
header. -
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.