How to use HTTP2 with Express.js and test it locally
Nick Scialli
December 03, 2022
Express.js is surprisingly lacking in HTTP/2 support. I have yet to find a resource that explains how to create an Express server with HTTP/2 support and demonstrates how to set up a self-signed SSL cert for local testing. In this post, we’ll accomplish the following:
- Create a minimal express server and observe it serving responses over HTTP/1.1
- Use the
spdy
package to create a HTTP/2 server - Generate a self-signed SSL cert for local testing
- Add some flexibility to run the app with or without https
Create an Express app
Let’s first create an express app in a new directory. For simplicity’s sake, I’ll just accept all the npm defaults:
npm init -y
npm install express
touch index.js
Now let’s write a “Hello world” endpoint in our app:
const express = require('express');
const PORT = 8000;
const app = express();
app.get('/', (req, res) => {
res.send('Hello world');
});
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
});
In our application’s directory, we can run node .
and when we navigate to http://localhost:8000 we will see “Hello world”. We can also pop open the network tab and note that this HTTP response was served using http/1.1
.
Adding spdy for HTTP/2
Express doesn’t have support for the native node http2
module; however, the spdy
package is a very popular alternative—at the time of writing this post, spdy
is averaging 10 million weeky downloads.
Let’s install spdy
and get our app working with http2
!
npm install spdy
Now we can make some updates to our index.js
file. Note that this won’t be functional yet—we’ll still need to do some SSL configuration.
const express = require('express');
const spdy = require('spdy');
const PORT = 8000;
const app = express();
app.get('/', (_, res) => {
res.send('hello world');
});
const server = spdy.createServer({}, app);
server.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log('SSL enabled');
});
Now let’s start our express app again:
node .
It’s important to know that no modern browsers support HTTP/2 on insecure connections, so the spdy
package will now be serving our app at https://localhost/8000. If we navigate to that URL, we should see an SSL warning that we can’t get around. Here’s what it looks like in Chrome:
Chrome is telling us that it doesn’t understand the protocol used by the SSL cert that our server is providing. That makes total sense because we’re not providing an SSL cert!
So how can we test this locally? We can generate a self-signed cert and pass that cert in the spdy
configuration object.
Creating a self-signed SSL certificate
To create a self-signed SSL cert, let’s first add some directories and scripts for repeatability—I’m making the assumption you’re on a development team and this process should be repeatable!
First, we’ll create a directory in which your cert and private key will live. We’ll also add an empty .keep
file in that directory. The .keep
file will make sure we check the directory in to Github despite having no other files in that directory.
mkdir cert
touch cert/.keep
Next, we’ll create a scripts
directory and a file for a script that will generate the cert.
mkdir scripts
touch scripts/generate-cert.sh
Edit the new generate-cert.sh
file and add the following code:
#!/bin/bash
openssl req -nodes -new -x509 \
-keyout ./cert/server.key \
-out ./cert/server.cert \
-subj "/C=US/ST=State/L=City/O=company/OU=Com/CN=www.testserver.local"
This is a single command that will generate an SSL cert and associated private key. Since this is a self-signed cert for local use, the subj
param doesn’t really matter and we can leave it as this generic value.
Note: This command assumes you have openssl
installed on your machine, which is generally the case in unix (Mac/Linux) environments. If you’re on Windows, you may need to find another way to generate a self-signed cert!
Finally, edit package.json
and add a command that will run the cert generation script:
{
"scripts": {
"generate-cert": "sh ./scripts/generate-cert.sh"
}
}
Assuming you are using git, you’ll want to make sure you git-ignore any generated certs/private keys. Create/edit a .gitignore
file in the project’s root directory and make sure it has at least the following items:
node_modules
*.key
*.cert
Now we have the ability to generate a self-signed cert and we made sure to ignore the generated files for git. Let’s give test it out:
npm run generate-cert
If all goes according to plan, you now have a server.cert
and server.key
file in the /cert
directory. Huzzah!
Providing the cert and key to our server
Now that we have a cert and private key, we can provide them to our HTTP/2 server. We’ll do this by using the built-in fs
module in index.js
:
const express = require('express');
const spdy = require('spdy');
const fs = require('fs');
const PORT = 8000;
const CERT_DIR = `${__dirname}/cert`;
const app = express();
app.get('/', (_, res) => {
res.send('hello world');
});
const server = spdy.createServer(
{
key: fs.readFileSync(`${CERT_DIR}/server.key`),
cert: fs.readFileSync(`${CERT_DIR}/server.cert`),
},
app
);
createServer();
server.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log('SSL Enabled');
});
Let’s now start our server and see the self-signed cert in action:
node .
If we go to https://localhost:8000 again, we see a new and exciting error message:
This is to be expected: your browser has no reason to trust the cert you generated locally. But this is fine: we know we generated it and it’s just for use during local development. In chrome, you can click “Proceed to localhost (unsafe)” to see the site running. Pop open the network tab and observe that the server is now using HTTP/2.
Congrats!
Bonus: allow local development over http
You may or may not want to do local development with https. You can give yourself the option by creating an environment variable to opt out of https for local development. Let’s create an SSL
environment variable and use that to opt in to using the HTTP/2 server:
const express = require('express');
const spdy = require('spdy');
const fs = require('fs');
const PORT = 8000;
const CERT_DIR = `${__dirname}/cert`;
const useSSL = !!process.env.SSL;
const app = express();
app.get('/', (_, res) => {
res.send('hello world');
});
function createServer() {
if (!useSSL) {
return app;
}
return spdy.createServer(
{
key: fs.readFileSync(`${CERT_DIR}/server.key`),
cert: fs.readFileSync(`${CERT_DIR}/server.cert`),
},
app
);
}
const server = createServer();
server.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log(`SSL ${useSSL ? 'enabled' : 'disabled'}`);
});
Now, we can serve the app using SSL + HTTP/2 with an environment variable:
SSL=true node .
Or we can use the old express server by not providing the environment variable:
node .
We have a lot of flexibility here: we can also just make this environment-dependent, have separate scripts in our package.json
file, or use some other mechanism to decide which server to use.
Conclusion
I was surprised to see the limited support for the native http2
node module with Express. Fortunately, setting Express with HTTP/2 is not so bad with the help of the spdy
package and self-signed certs.
Nick Scialli is a senior UI engineer at Microsoft.