Setup HTTP/2 Connections in a Native Node.js Server

This article provides a straightforward guide on how to configure and manage HTTP/2 connections using the native http2 module in Node.js. You will learn how to initialize a secure HTTP/2 server, handle incoming streams, and send multiplexed responses to the client without relying on third-party frameworks.


Why Use Native HTTP/2?

HTTP/2 improves web performance through multiplexing (sending multiple requests and responses concurrently over a single TCP connection), header compression (HPACK), and server push capabilities. Node.js features a built-in http2 module that allows you to leverage these protocol advantages directly.

Because modern web browsers require TLS (encrypted connections) to use HTTP/2, you must configure a secure server (HTTP/2 over TLS, or h2).

Step 1: Generate SSL/TLS Certificates

To establish a secure connection, you need a private key and a self-signed certificate. You can generate these using OpenSSL in your terminal:

openssl req -x509 -newkey rsa:2048 -nodes -sha256 -keyout key.pem -out cert.pem -days 365

Step 2: Create the HTTP/2 Server

Use the native Node.js http2 module to load your certificates and create a secure server instance. Unlike HTTP/1.1, which relies on standard request and response objects, HTTP/2 natively operates on “streams.”

Create a file named server.js and add the following code:

const http2 = require('node:http2');
const fs = require('node:fs');
const path = require('node:path');

// Read the SSL certificate and private key
const serverOptions = {
  key: fs.readFileSync(path.join(__dirname, 'key.pem')),
  cert: fs.readFileSync(path.join(__dirname, 'cert.pem')),
};

// Create the secure HTTP/2 server
const server = http2.createSecureServer(serverOptions);

// Log server errors
server.on('error', (err) => console.error('Server error:', err));

// Listen for incoming streams
server.on('stream', (stream, headers) => {
  const requestPath = headers[':path'];
  const requestMethod = headers[':method'];

  console.log(`Received ${requestMethod} request for ${requestPath}`);

  // Route handling
  if (requestPath === '/' && requestMethod === 'GET') {
    // Send headers using stream.respond()
    stream.respond({
      'content-type': 'text/html; charset=utf-8',
      ':status': 200,
    });
    
    // Write the body and close the stream
    stream.end('<h1>Hello HTTP/2 from Native Node.js!</h1>');
  } else if (requestPath === '/api/data' && requestMethod === 'GET') {
    stream.respond({
      'content-type': 'application/json',
      ':status': 200,
    });
    stream.end(JSON.stringify({ status: 'success', data: 'Multiplexing works!' }));
  } else {
    stream.respond({
      ':status': 404,
    });
    stream.end('Page Not Found');
  }
});

// Start the server
const PORT = 8443;
server.listen(PORT, () => {
  console.log(`HTTP/2 server running at https://localhost:${PORT}`);
});

Step 3: Test Your HTTP/2 Server

Run your server using Node.js:

node server.js

To verify that the server is communicating over HTTP/2, use the curl tool in your command line with the --http2 flag. The -k flag is necessary to bypass SSL warnings if you are using a self-signed certificate:

curl -I -k --http2 https://localhost:8443/

You should see :status: 200 and HTTP/2 indicated in the response headers.

Handling Client Disconnections

HTTP/2 streams can be aborted by the client at any time. It is important to handle the close or error events on individual streams to prevent your server from leaking resources:

server.on('stream', (stream, headers) => {
  stream.on('error', (err) => {
    console.error('Stream error:', err);
  });

  stream.on('close', () => {
    console.log('Stream closed');
  });
  
  // Respond to the stream...
});