HTTP/3 servers in Node.js don’t actually run HTTP/3 directly; they run QUIC, and HTTP/3 is just the first protocol on top of QUIC.
Let’s see it in action. We’ll set up a simple HTTP/3 server using the node-quic library and then fetch a resource from it.
# First, install the necessary packages
npm install node-quic express q
Now, here’s the server code (server.js):
const http3 = require('node-quic');
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
// Serve static files from a 'public' directory
app.use(express.static(path.join(__dirname, 'public')));
// A simple API endpoint
app.get('/api/hello', (req, res) => {
res.send('Hello from HTTP/3!');
});
// Load SSL certificates
const cert = fs.readFileSync('./certs/cert.pem');
const key = fs.readFileSync('./certs/key.pem');
// Create the QUIC server
const server = http3.createQuicServer({
key: key,
cert: cert,
alpn: 'h3', // Application-Layer Protocol Negotiation for HTTP/3
});
server.on('session', (session) => {
console.log(`New QUIC session from ${session.remoteAddress}:${session.remotePort}`);
session.on('stream', (stream) => {
console.log(`New stream opened`);
// Handle incoming HTTP/3 requests
stream.on('data', (data) => {
const request = data.toString();
console.log('Received request:', request);
// For simplicity, we'll just respond to everything with a basic HTTP/3 response.
// A real application would parse the request and route it.
const responseBody = `
HTTP/3 200 OK
Content-Type: text/plain
Content-Length: ${Buffer.byteLength('Hello from HTTP/3!')}
Connection: close
Hello from HTTP/3!
`;
stream.write(responseBody);
stream.end();
});
stream.on('error', (err) => {
console.error('Stream error:', err);
});
});
session.on('error', (err) => {
console.error('Session error:', err);
});
});
// Start the server on port 443 (standard HTTPS/QUIC port)
server.listen(443, () => {
console.log('HTTP/3 server listening on port 443');
});
// --- For testing, we need a self-signed certificate ---
// Create a 'certs' directory if it doesn't exist
if (!fs.existsSync('./certs')) {
fs.mkdirSync('./certs');
}
// Generate a self-signed certificate and key (run this once)
// You can use openssl for this:
// openssl req -x509 -newkey rsa:2048 -nodes -keyout certs/key.pem -out certs/cert.pem -days 365 -subj "/CN=localhost"
Before running, you’ll need to generate a self-signed certificate and key. Run this in your terminal:
mkdir certs
openssl req -x509 -newkey rsa:2048 -nodes -keyout certs/key.pem -out certs/cert.pem -days 365 -subj "/CN=localhost"
Now, start the server:
node server.js
And here’s how you’d fetch from it using curl (make sure your curl supports HTTP/3, usually with --http3):
curl --http3 -k https://localhost/api/hello
You should see Hello from HTTP/3!. The -k flag is necessary because we’re using a self-signed certificate.
The core of this is the http3.createQuicServer function. It takes your SSL certificate and key, and importantly, the alpn: 'h3' option. ALPN (Application-Layer Protocol Negotiation) is a TLS extension that allows the client and server to agree on the protocol to use over the secure connection. For HTTP/3, this protocol is h3.
When a client connects, the server.on('session', ...) event fires. Each session object represents a QUIC connection. Within a session, data is transmitted over streams. The session.on('stream', ...) event is triggered when a new logical stream is opened by the client. Your server then receives data on that stream and can write a response back to it, mimicking the HTTP/3 request/response flow. The node-quic library handles the complex QUIC transport layer, including UDP packet management, connection establishment, flow control, and reliability. Your code focuses on the application-level protocol (HTTP/3) running over these streams.
The most surprising thing about HTTP/3 is that it doesn’t guarantee in-order delivery of frames within the HTTP/3 protocol itself, even though QUIC streams do guarantee in-order delivery of data within that specific stream. This is because HTTP/3 can use multiple independent streams for different requests, and if one stream is blocked by packet loss (e.g., a HEAD request waiting for a small response), other streams (e.g., a GET request for a large image) can proceed without waiting. This is a major departure from HTTP/1.1 and HTTP/2, which suffered from "head-of-line blocking" at the HTTP layer.
The node-quic library is a lower-level abstraction than you might expect if you’re coming from http or https modules. It exposes QUIC sessions and streams, and you’re responsible for implementing the HTTP/3 framing and request parsing on top of those streams. Libraries like h3 can provide a higher-level HTTP/3 abstraction over node-quic.
The next thing you’ll likely want to explore is how to handle multiple concurrent streams efficiently and how to implement proper HTTP/3 request parsing and routing using a dedicated HTTP/3 library.