ReferenceRuntime Adapters
@nextrush/adapter-node

Node.js

Node.js HTTP adapter for NextRush

Connect NextRush to Node.js's built-in HTTP server.

Source & internals

Package@nextrush/adapter-node
StatusStable
Support tierPublic — core (stable, semver-guarded)
Included in nextrush?Yes — createApp/listen use this adapter on Node
RuntimeNode.js >=22 only

Included in nextrush

This adapter is included when you install the nextrush package. Install separately only when using other runtimes alongside Node.js.

$ pnpm add @nextrush/adapter-node

Node.js 22+ Required

NextRush requires Node.js 22.0.0 or later. Earlier versions are not supported.

What It Provides

The adapter exports server lifecycle functions and context utilities:

// Server functions
import { serve, listen, createHandler } from '@nextrush/adapter-node';
import type { ServeOptions, ServerInstance } from '@nextrush/adapter-node';

// Context (advanced)
import { NodeContext, createNodeContext } from '@nextrush/adapter-node';

// Body source (advanced)
import {
  NodeBodySource,
  createNodeBodySource,
  createEmptyBodySource,
} from '@nextrush/adapter-node';

Quick Start

import { createApp } from '@nextrush/core';
import { createRouter } from '@nextrush/router';
import { serve } from '@nextrush/adapter-node';

const app = createApp();
const router = createRouter();

router.get('/', (ctx) => {
  ctx.json({ message: 'Hello from Node.js!' });
});

app.route('/', router);

await serve(app, {
  port: 8080,
  onListen: ({ port }) => console.log(`Listening on port ${port}`),
});

Using the Meta Package

The nextrush package includes this adapter:

import { createApp, createRouter, listen } from 'nextrush';

const app = createApp();
const router = createRouter();

router.get('/', (ctx) => ctx.json({ hello: 'world' }));

app.route('/', router);
await listen(app, 8080);

serve()

Start an HTTP server with full configuration:

import { serve } from '@nextrush/adapter-node';

const server = await serve(app, {
  port: 8080,
  host: '0.0.0.0',
  timeout: 30000,
  keepAliveTimeout: 5000,
  onListen: ({ port, host }) => {
    console.log(`Server running at http://${host}:${port}`);
  },
  onError: (error) => {
    console.error('Server error:', error);
  },
});

// Later: graceful shutdown
await server.close();

Options

ServeOptions

PropertyTypeDescription
portnumber= 8080Port to listen on
hoststring= '0.0.0.0'Host to bind to
tls?{ cert: string | Buffer; key: string | Buffer; ca?: string | Buffer }Enables HTTPS and negotiates HTTP/2 automatically via ALPN. Clients that do not offer h2 fall back to HTTP/1.1 over TLS.
timeoutnumber= 30000Request timeout in milliseconds
keepAliveTimeoutnumber= 5000Keep-alive timeout in milliseconds
shutdownTimeoutnumber= 30000Graceful shutdown timeout in milliseconds. Forces closure if connections do not drain within this time.
onListen?(info: { port: number; host: string }) => voidCallback when server starts listening
onError?(error: Error) => voidCustom error handler for uncaught server errors
logger?LoggerLogger for adapter diagnostics. Defaults to app.logger.
interface ServerInstance {
  server: Server; // Node.js http.Server
  port: number; // Listening port
  host: string; // Bound host
  address(): { port: number; host: string }; // Get address info
  close(): Promise<void>; // Graceful shutdown
}

listen()

Shorthand for common case with default logging:

import { listen } from '@nextrush/adapter-node';

await listen(app, 8080);
// Output: 🚀 NextRush listening on http://localhost:8080

createHandler()

Create a request handler for custom server setup.

Signature:

function createHandler(
  app: Application,
  options?: { logger?: Logger }
): (req: IncomingMessage, res: ServerResponse) => void;
import { createHandler } from '@nextrush/adapter-node';
import { createServer } from 'node:http';
import { createServer as createHttpsServer } from 'node:https';

const handler = createHandler(app);

// Use with http
const httpServer = createServer(handler);

// Use with https
const httpsServer = createHttpsServer(
  {
    key: privateKey,
    cert: certificate,
  },
  handler
);

HTTPS & HTTP/2

Pass a tls option to serve() to enable HTTPS. HTTP/2 is negotiated automatically via ALPN — clients that support it get h2; everything else falls back to HTTPS/1.1 transparently. There is no separate "protocol" option to configure — negotiation is the platform's job, not the application's.

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

const app = createApp();
// ... configure app

await serve(app, {
  port: 443,
  tls: {
    cert: readFileSync('certificate.pem'),
    key: readFileSync('private-key.pem'),
    ca: readFileSync('ca.pem'), // optional — custom trust chain
  },
});

See it end to end

For a full local-cert-to-production walkthrough (generating a dev certificate, verifying HTTP/2 negotiated, deploying behind a reverse proxy), see the HTTPS & HTTP/2 guide.

Need a raw (req, res) handler instead — for example, to hand off to your own node:https/node:http2 server setup, or a test harness that brings its own http.Server? Use createHandler():

import { createHandler } from '@nextrush/adapter-node';
import { createServer } from 'node:https';
import { readFileSync } from 'node:fs';

const handler = createHandler(app);

const server = createServer(
  { key: readFileSync('private-key.pem'), cert: readFileSync('certificate.pem') },
  handler
);

server.listen(443);

Graceful Shutdown

import { serve } from '@nextrush/adapter-node';

const server = await serve(app, { port: 8080 });

// Handle shutdown signals
async function shutdown() {
  console.log('Shutting down...');
  await server.close();
  console.log('Server closed');
  process.exit(0);
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

Or wire it declaratively instead of listening for signals yourself:

const server = await serve(app, {
  port: 8080,
  gracefulShutdown: true, // installs SIGTERM + SIGINT, using shutdownTimeout as the drain window
});

// Or override the signal set / drain timeout explicitly:
const server2 = await serve(app, {
  port: 8080,
  gracefulShutdown: { signals: ['SIGTERM'], timeout: 5_000 },
});

Omitting gracefulShutdown installs no signal handler — process behavior is unchanged. Registering a signal handler (either way) changes Node's default behavior for that signal (immediate exit) — coordinate directly if your own code also listens for the same signal.

Accessing Raw Objects

Access Node.js IncomingMessage and ServerResponse when needed:

router.get('/raw', (ctx) => {
  const { req, res } = ctx.raw;

  // Access Node.js-specific properties
  const remoteAddress = req.socket.remoteAddress;
  const httpVersion = req.httpVersion;

  // Direct response manipulation (escape hatch)
  res.writeHead(200, { 'X-Custom': 'value' });
  res.end('Raw response');
});

Use Raw Objects Carefully

When using ctx.raw, you bypass NextRush's response handling. Make sure you don't mix raw and NextRush response methods.

Request Paths and Dot Segments

Dot segments are rejected here — this platform preserves the target

Node's HTTP server hands the request line to the application unparsed, so a target containing a dot segment — /files/../admin, or the encoded /files/%2e%2e/admin — is rejected with 400 before any middleware runs, and ctx.path holds the bytes the client sent.

That guarantee ends at this process's edge: put a proxy in front that resolves the segment first and the framework never sees it. Authorise path rules against ctx.path, and reject unwanted raw targets at the proxy. See Request Paths and Dot Segments.


Performance Tuning

Timeouts

await serve(app, {
  port: 8080,
  timeout: 60000, // 60s request timeout
  keepAliveTimeout: 10000, // 10s keep-alive
});

Cluster Mode

Scale across CPU cores:

import cluster from 'node:cluster';
import { cpus } from 'node:os';
import { createApp } from '@nextrush/core';
import { listen } from '@nextrush/adapter-node';

if (cluster.isPrimary) {
  const numCPUs = cpus().length;
  console.log(`Primary ${process.pid} starting ${numCPUs} workers`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code) => {
    console.log(`Worker ${worker.process.pid} died (${code})`);
    cluster.fork(); // Replace dead worker
  });
} else {
  const app = createApp();
  // ... configure app

  await listen(app, 8080);
  console.log(`Worker ${process.pid} started`);
}

Complete Example

import { createApp } from '@nextrush/core';
import { createRouter } from '@nextrush/router';
import { serve } from '@nextrush/adapter-node';

const app = createApp({
  env: process.env.NODE_ENV as 'production' | 'development',
});

// Middleware
app.use(async (ctx) => {
  const start = Date.now();
  await ctx.next();
  console.log(`${ctx.method} ${ctx.path} - ${Date.now() - start}ms`);
});

// Error handler
app.setErrorHandler((error, ctx) => {
  console.error('Request error:', error);
  ctx.status = 500;
  ctx.json({ error: 'Internal Server Error' });
});

// Routes
const router = createRouter();

router.get('/health', (ctx) => {
  ctx.json({ status: 'healthy', runtime: 'node' });
});

router.get('/users/:id', (ctx) => {
  ctx.json({ id: ctx.params.id });
});

app.route('/', router);

// Start server
const server = await serve(app, {
  port: Number(process.env.PORT) || 8080,
  onListen: ({ port }) => {
    console.log(`🚀 Server running on http://localhost:${port}`);
  },
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await server.close();
  process.exit(0);
});

TypeScript Types

import type { ServeOptions, ServerInstance } from '@nextrush/adapter-node';

import type { Application } from '@nextrush/core';

import type { IncomingMessage, ServerResponse } from 'node:http';

See Also

Was this helpful?

On this page