Node.js Native HTTP/3
Native addon via napi-rs. Real QUIC server calling your JS handler directly. No proxy.
Setup
cd nhttp3-node
npm install
npx napi build --release
Express-like Server
const { serve } = require('nhttp3-node');
serve(4433, (req) => {
console.log(`${req.method} ${req.path}`);
if (req.path === '/') {
return {
status: 200,
headers: [['content-type', 'application/json']],
body: JSON.stringify({ hello: 'world', protocol: 'h3' }),
};
}
return { status: 404, headers: [], body: 'not found' };
});
Test
# Terminal 1: start Node.js HTTP/3 server
node test.js
# Terminal 2: hit it with the client
cargo run -p nhttp3-server --bin nhttp3-client -- https://localhost:4433/
# → {"hello":"world","protocol":"h3"}
QPACK Compression
const { encodeHeaders, decodeHeaders } = require('nhttp3-node');
const headers = [[':method', 'GET'], [':path', '/api'], ['accept', 'application/json']];
const encoded = encodeHeaders(headers);
const decoded = decodeHeaders(encoded);
// 3 headers → 28 bytes → 3 decoded (roundtrip verified)
How it Works
The addon embeds quinn (Rust QUIC library) and runs a tokio runtime. When an HTTP/3 request arrives, it calls your JS handler via napi-rs ThreadsafeFunction. The response goes directly back over the QUIC stream.
HTTP/3 request → quinn → h3 → napi → JS callback → napi → h3 → quinn → QUIC response
No proxy
This is not a proxy in front of Express. The QUIC server runs inside your Node.js process. Your handler is called directly for each HTTP/3 request.
Handler API
// Request object
{
method: 'GET', // HTTP method
path: '/api/users', // Request path
headers: [['accept', '*/*']], // [name, value] pairs
body: Buffer // Request body (for POST/PUT)
}
// Return a response object
{
status: 200,
headers: [['content-type', 'application/json']],
body: '{"ok": true}'
}
Source
nhttp3-node/ — Cargo.toml, Rust addon, JS wrapper, test file.
Agent-friendly copy
Markdown: docs/guides/nodejs-server.md