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