@nextrush/adapter-nodeNode.js
Node.js HTTP adapter for NextRush
Connect NextRush to Node.js's built-in HTTP server.
Source & internals
README · ARCHITECTURE · see also Adapter Contract
| Package | @nextrush/adapter-node |
| Status | Stable |
| Support tier | Public — core (stable, semver-guarded) |
Included in nextrush? | Yes — createApp/listen use this adapter on Node |
| Runtime | Node.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
| Property | Type | Description |
|---|---|---|
port | number= 8080 | Port to listen on |
host | string= '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. |
timeout | number= 30000 | Request timeout in milliseconds |
keepAliveTimeout | number= 5000 | Keep-alive timeout in milliseconds |
shutdownTimeout | number= 30000 | Graceful shutdown timeout in milliseconds. Forces closure if connections do not drain within this time. |
onListen? | (info: { port: number; host: string }) => void | Callback when server starts listening |
onError? | (error: Error) => void | Custom error handler for uncaught server errors |
logger? | Logger | Logger 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:8080createHandler()
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';