beta

Node Server

Node.js HTTP server adapter with optimized request/response handling.

@miiajs/node-server provides a high-performance Node.js HTTP server adapter for MiiaJS.

Installation

bun add @miiajs/node-server

Usage

import { Miia } from '@miiajs/core'
import { serve } from '@miiajs/node-server'

const app = new Miia().register(AppModule)

await app.listen(3000, serve)

With custom hostname:

await app.listen(3000, 'localhost', serve)

How it works

The serve function bridges Node.js's http.IncomingMessage/http.ServerResponse and the Web Standard Request/Response API that MiiaJS uses internally.

Performance modes

The adapter supports two modes:

Optimized mode (default)

Minimizes allocations on the hot path:

  • Lazy Request Proxy - lightweight object instead of new Request(). Method, URL, headers, and body are resolved only on first access.
  • Lightweight Headers - linear scan over raw header pairs instead of constructing a Headers object. Covers .get(), .has(), .forEach(), and iterators.
  • Body Buffering - small POST bodies (Content-Length ≤ bufferThreshold) are buffered and parsed directly, bypassing ReadableStream and Request creation. Large or chunked bodies fall back to streaming via Readable.toWeb().
  • LightResponse Cache - simple responses (string, null, Uint8Array) store a [status, body, headers] tuple without creating a real Response object.
  • Sync Fast Path - synchronous handlers bypass Promise allocation entirely.

Native mode

Full Web API compliance with standard Request and Response objects:

import { serve } from '@miiajs/node-server'

const server = await serve({
  fetch: app.fetch,
  port: 3000,
  mode: 'native',
})

Use native mode when you need strict instanceof Response checks or run multiple frameworks in the same process.

Standalone usage

The adapter can be used without the MiiaJS framework:

import { serve } from '@miiajs/node-server'

const server = await serve({
  fetch: (req) => new Response('Hello, World!'),
  port: 3000,
  hostname: '0.0.0.0',
})

// Later...
await server.close()

Options

interface ServeOptions {
  fetch: (req: Request) => Response | Promise<Response>  // Required
  port?: number              // Default: 3000
  hostname?: string          // Default: '0.0.0.0'
  mode?: 'optimized' | 'native'  // Default: 'optimized'
  bufferThreshold?: number   // Default: 102400 (100KB)
}

The bufferThreshold controls the body buffering optimization in optimized mode. POST/PUT/PATCH bodies with a known Content-Length up to this size are buffered in memory for fast json()/text() access. Bodies without Content-Length or larger than the threshold use streaming via Readable.toWeb().

ServerHandle

The serve function returns a ServerHandle with a close() method:

const server = await serve({ fetch: handler, port: 3000 })

// Graceful shutdown
await server.close()

Testing

@miiajs/node-server is exercised directly - serve() accepts any (req: Request) => Response | Promise<Response>, so you don't need a Miia instance to test it.

import { afterEach, expect, it } from 'bun:test'
import { serve, type ServerHandle } from '@miiajs/node-server'

let server: ServerHandle

afterEach(async () => {
  if (server) await server.close()
})

it('handles GET requests', async () => {
  server = await serve({
    port: 18234,
    fetch: () => Response.json({ ok: true }),
  })

  const res = await fetch('http://localhost:18234/')
  expect(res.status).toBe(200)
  expect(await res.json()).toEqual({ ok: true })
})

For applications that wire the adapter through app.listen(port, host, serve), prefer the TestApp pattern from Testing - it runs handlers without binding a port and works under any runtime.