Node.js HTTP Server
Introduction
Section titled “Introduction”@xeno-js/node connects a Xeno.JS application to Node.js’s built-in HTTP
server. Use it when you want to run the application as a Node.js process
without selecting a separate web server framework.
The plugin configures the transport. Your application still defines routes, registers controllers and services, and configures any required middleware. This keeps the application boundary separate from the choice of server.
The setup is:
- Install the plugin and its core peer dependency.
- Register routes, services, and
withNodeHttpServer()inXenoAppBuilder. - Build the application and start the listener.
Installation
Section titled “Installation”npm install @xeno-js/core @xeno-js/nodeThe package declares Node.js >=20 and @xeno-js/core as a peer
dependency. Select compatible package versions for your application.
Start a server
Section titled “Start a server”This example registers a controller and a route, then listens on the local interface. The controller returns a fixed order; replace it with application services for a real endpoint.
import { HttpHelper, XenoAppBuilder } from '@xeno-js/core'import type { IController, NativeRouteDefinition, ResponseDto, XenoRegistry,} from '@xeno-js/core'import { withNodeHttpServer } from '@xeno-js/node'
interface OrderSummary { id: string status: string}
interface OrdersRegistry extends XenoRegistry { ORDER_STATUS_CONTROLLER: IController<void, OrderSummary>}
class OrderStatusController implements IController<void, OrderSummary> { async handle(): Promise<ResponseDto<OrderSummary>> { return HttpHelper.success({ id: '42', status: 'received' }) }}
const routes: NativeRouteDefinition<OrdersRegistry>[] = [ { method: 'GET', path: '/api/orders/current', handler: async (ctx) => ctx.handle('ORDER_STATUS_CONTROLLER'), },]
async function main() { const app = await new XenoAppBuilder<OrdersRegistry>() .addRoutes(routes) .addServices((services) => { services.addScoped( 'ORDER_STATUS_CONTROLLER', () => new OrderStatusController(), ) }) .addHttpServer(withNodeHttpServer<OrdersRegistry>()) .build()
await app.listen({ host: '127.0.0.1', port: 3000, onListening: ({ host, port }) => { console.info(`Server listening on ${host}:${port}`) }, })}
main().catch((error: unknown) => { console.error('Application startup failed', error) process.exitCode = 1})After startup, request the endpoint:
curl http://127.0.0.1:3000/api/orders/currentThe server returns status 200 and the controller’s success payload:
{ "success": true, "data": { "id": "42", "status": "received" }, "meta": {}}build() initializes the queued modules. listen() resolves and starts the
Node server; onListening runs after the listener starts. The example uses
ctx.handle(), so controller dispatch uses a request scope. It does not
configure the full HTTP middleware or authentication; configure these before
exposing endpoints that require them.
The catch above reports rejected bootstrap and startup operations, but the
current Node server implementation does not attach a listener that rejects
the promise on Node’s asynchronous server error event. For example, an
occupied port can emit an unhandled error instead of reaching that catch.
Do not treat await app.listen() as complete coverage of listener errors in
this integration; account for this limitation in deployment monitoring and
testing.
Configure the listener
Section titled “Configure the listener”withNodeHttpServer() takes no arguments. Configure the listener with
app.listen():
| Option | Type | Use |
|---|---|---|
host |
string |
Network interface or host on which to listen. |
port |
number |
TCP port on which to listen. |
onListening |
({ host, port }) => void |
Callback invoked when the listener starts. |
You can omit host, port, or the whole options object:
// After registering routes and the Node server and building the application.await app.listen()Xeno.JS selects each value in this order:
- The value passed to
listen(). HOSTorPORTfrom the configuration service.0.0.0.0for the host and3000for the port.
The default Node configuration service reads environment variables; it does
not load a .env file by itself. 0.0.0.0 listens on all IPv4 interfaces.
Use 127.0.0.1 for local-only access, or set the host required by the
deployment. The options factory reads PORT even if an explicit port is
provided, so a non-numeric PORT causes a configuration error.
onListening receives the configured host and port, not the resolved server
address. In particular, if you specify port 0, its port is still 0,
not the operating system’s selected port. The public plugin does not expose
the native http.Server instance through app.listen().
The plugin uses node:http, not a TLS listener. If clients require HTTPS,
terminate TLS through a properly configured reverse proxy or hosting platform.
The plugin does not expose native server construction options through
withNodeHttpServer().
Register routes and middleware
Section titled “Register routes and middleware”Use addRoutes() before build(). The route module provides the dispatch
services needed by the Node integration. To start a listener without
endpoints, register an empty array with addRoutes([]); requests then
receive 404. Registering the server plugin alone does not provide all the
dispatcher dependencies needed for listen().
For application routes, register the controller token and its implementation as in the example above. See HTTP Routes for route definitions, parameters, matching, and policies.
Use addMiddlewares() to configure HTTP middleware such as CORS, rate
limiting, and CSRF. Authentication and authorization need their own
configuration. Route handlers that return a response directly do not run the
ctx.handle() middleware dispatch path. If your application is behind a
reverse proxy, configure forwarded headers and trusted proxies only for
proxies you control. See HTTP Server for a middleware example.
Requests and responses
Section titled “Requests and responses”The Node integration maps native requests to Xeno.JS request objects and
passes matching requests to the registered route. For controller responses,
it writes the ResponseDto status and headers and serializes
ResponseDto.data as the body. See API Responses for
success and error payloads.
Some transport-specific behavior matters when configuring an application:
- The Node integration buffers the body of a matching request in memory before dispatch. It does not expose a request-body size limit through this plugin; enforce an appropriate limit at a trusted proxy or another boundary.
GET,HEAD, andOPTIONSbodies are not read by the Node mapper.HttpRouteParamHelper.extractInput()also does not readDELETEbodies; consult HTTP Helpers before designing body handling.- The adapter does not forward the incoming transport signal into the
Requestused for controller dispatch. Do not rely on client disconnects to cancel controller work. - An unmatched route returns status
404and a body such as{ "error": "Route GET /missing not found" }. This is not the standard controller error envelope. - When a route or dispatch throws outside the controller’s formatted
response path, the Node integration returns
500with the thrown error’s message in anerrorfield. Avoid exposing sensitive exception messages; handle expected failures explicitly and protect the deployment boundary.
Stop the server
Section titled “Stop the server”After startup, the registered HTTP server exposes dispose():
// app is the application returned by build().await app.getServices().httpServer.dispose()The Node integration closes its listener and clears its server reference.
This call closes the HTTP server only; it is not an application-wide
shutdown API and does not by itself dispose all other registered services.
Coordinate shutdown of the rest of the application and external resources
according to your deployment’s lifecycle. Avoid calling listen() repeatedly
on the same server instance without closing it.
Next steps
Section titled “Next steps”- Configure host, middleware, and other transports in HTTP Server.
- Define endpoints in HTTP Routes.
- Implement controller responses in HTTP Controllers and API Responses.
- Review Context and Scopes for request context and scoped services.
Support Us
Section titled “Support Us”Xeno.JS is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section.
