GuidesAPI Development

HTTPS & HTTP/2

Enable TLS and negotiate HTTP/2 automatically, without changing your application code

Your route handlers, middleware, and Context never know or care which transport a request arrived over. Enabling HTTPS — and getting HTTP/2 for clients that support it — is entirely a serve() option; nothing about how you write handlers changes.

Why there's no "protocol" option

HTTP/2 is negotiated during the TLS handshake via ALPN (Application-Layer Protocol Negotiation) — the client and server agree on h2 or http/1.1 before any HTTP data is exchanged. Because of that, NextRush never exposes a protocol: 'http2' option to choose from. You configure TLS; the runtime negotiates the rest. A client that doesn't support HTTP/2 falls back to HTTPS/1.1 transparently, with identical framework behavior either way.

Enable it

import { createApp } from '@nextrush/core';
import { serve } from '@nextrush/adapter-node';
import { readFileSync } from 'node:fs';

const app = createApp();
app.use(async (ctx) => ctx.json({ hello: 'world' }));

await serve(app, {
  port: 443,
  tls: {
    cert: readFileSync('certificate.pem'),
    key: readFileSync('private-key.pem'),
  },
});
import { createApp } from '@nextrush/core';
import { serve } from '@nextrush/adapter-bun';

const app = createApp();
app.use(async (ctx) => ctx.json({ hello: 'world' }));

await serve(app, {
  port: 443,
  tls: {
    cert: Bun.file('./certificate.pem'),
    key: Bun.file('./private-key.pem'),
  },
});

Bun does not negotiate HTTP/2 via this option

Verified against Bun.serve() directly: its native tls option serves HTTPS/1.1 only — it does not negotiate h2 via ALPN. getRuntimeCapabilities().http2 correctly reports false on Bun. If you need HTTP/2 specifically on Bun today, it isn't available through this adapter yet; TLS/HTTPS works exactly as shown above.

import { createApp } from '@nextrush/core';
import { serve } from '@nextrush/adapter-deno';

const app = createApp();
app.use(async (ctx) => ctx.json({ hello: 'world' }));

await serve(app, {
  port: 443,
  tls: {
    cert: await Deno.readTextFile('./certificate.pem'),
    key: await Deno.readTextFile('./private-key.pem'),
  },
});

Deno.serve() negotiates HTTP/2 via ALPN automatically once tls is present — no extra configuration needed.

The tls shape ({ cert, key, ca? }) is identical across Node and Bun; Deno also accepts it as of this shape's introduction, deprecating its previous flat cert/key options (see the Deno adapter reference for the migration note if you're on the old fields).

Query the capability instead of branching on the runtime

If your code needs to know whether the current runtime actually negotiates HTTP/2, query RuntimeCapabilities — never branch on which runtime you're on:

import { getRuntimeCapabilities } from '@nextrush/runtime';

const { secureServing, http2 } = getRuntimeCapabilities();

if (http2) {
  // safe to assume h2 negotiates when tls is configured
}

This is the same capability-negotiation model every other runtime-varying behavior in NextRush uses — see Runtime Compatibility for the underlying model.

Generate a local development certificate

For local testing, a self-signed certificate is enough:

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
  -days 365 -nodes -subj "/CN=localhost"

Browsers and most HTTP clients will warn about the untrusted self-signed cert — that's expected locally. For a real HTTP/2 client to verify the negotiation, you can also check with curl:

curl -k --http2 -v https://localhost:443/ 2>&1 | grep "using HTTP"

Verify HTTP/2 is actually negotiating

Because negotiation happens transparently, a shipped tls config can go unverified — nothing fails if h2 silently isn't being selected. A quick way to check from Node itself:

import http2 from 'node:http2';

const client = http2.connect('https://localhost:443', { rejectUnauthorized: false });
client.on('connect', () => {
  console.log('Negotiated:', client.alpnProtocol); // 'h2' or 'http/1.1'
  client.close();
});

Production deployment

Terminating TLS inside your own process (as shown above) is one option. Many production deployments instead terminate TLS at a reverse proxy or load balancer (nginx, Caddy, a cloud load balancer) and run NextRush behind it in plain HTTP — both are valid; this guide covers configuring TLS inside the adapter when you do want to terminate it yourself. For platform-specific deployment concerns (Docker, Vercel, Lambda, Cloudflare), see production/deployment.

Edge runtimes

On Cloudflare Workers, Vercel Edge, and other edge platforms, TLS termination and protocol negotiation are handled entirely by the hosting platform before your code ever runs — there is no tls option on @nextrush/adapter-edge, and none is needed.

Was this helpful?

On this page