HTTP Routes
Introduction
Section titled “Introduction”An HTTP application needs to map incoming methods and paths to application operations. Xeno.JS expresses these mappings as route definitions, separately from the server integration and the controller implementation.
Each route specifies:
- an HTTP method;
- a path, optionally containing named parameters;
- a handler that receives the request and can dispatch to a controller;
- optional security settings for the route.
Register the definitions with addRoutes() during application composition.
The server integration receives requests; routes select the application
operation to run. For server setup, see HTTP Server.
Define and register a route
Section titled “Define and register a route”The following example defines a route and a controller, registers both, and starts a Node.js server. It uses a fixed response to keep the example focused on the route; a real controller would delegate to application services.
npm install @xeno-js/core @xeno-js/nodeimport { HttpHelper, HttpRouteParamHelper, XenoAppBuilder,} from '@xeno-js/core'import type { IController, NativeRouteDefinition, ResponseDto, XenoRegistry,} from '@xeno-js/core'import { withNodeHttpServer } from '@xeno-js/node'
interface GetOrderInput { id: string}
interface OrderSummary { id: string status: string}
interface OrdersRegistry extends XenoRegistry { GET_ORDER_CONTROLLER: IController<GetOrderInput, OrderSummary>}
class GetOrderController implements IController<GetOrderInput, OrderSummary> { async handle(input: GetOrderInput): Promise<ResponseDto<OrderSummary>> { return HttpHelper.success({ id: input.id, status: 'received' }) }}
const orderRoute: NativeRouteDefinition<OrdersRegistry> = { method: 'GET', path: '/api/orders/:id', handler: async (ctx) => { const input = await HttpRouteParamHelper.extractInput<GetOrderInput>( ctx.req, '/api/orders/:id', ) return ctx.handle('GET_ORDER_CONTROLLER', input) },}
async function main() { const app = await new XenoAppBuilder<OrdersRegistry>() .addRoutes([orderRoute]) .addServices((services) => { services.addScoped('GET_ORDER_CONTROLLER', () => new GetOrderController()) }) .addHttpServer(withNodeHttpServer<OrdersRegistry>()) .build()
await app.listen({ host: '127.0.0.1', port: 3000 })}
main().catch((error: unknown) => { console.error('Failed to start the application', error) process.exitCode = 1})A GET request to /api/orders/42 matches the route. The handler extracts
an input containing id: '42' and passes it to the registered controller.
HttpHelper.success creates a ResponseDto with status 200 and a JSON
response body:
{ "success": true, "data": { "id": "42", "status": "received" }, "meta": {}}The registry describes valid service keys at compile time. addServices()
registers the controller at runtime; defining a key in the registry alone
does not register it. The controller resolves within the dispatch scope.
addScoped() reuses an instance within that scope; addSingleton() shares an
instance across the container, while addTransient() creates one per
resolution. Do not inject a scoped dependency into a singleton. See
Dependency Injection for lifetimes.
This example does not call addMiddlewares(). Xeno.JS supplies a minimal
dispatch context for the controller, but it does not enable the configured
CORS, authentication, rate limiting, or CSRF middleware. Configure those
explicitly when the application needs them.
Route definition
Section titled “Route definition”NativeRouteDefinition<TRegistry> has the following properties:
| Property | Purpose |
|---|---|
method |
HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS. |
path |
Route path, with optional named segments such as :id or {id}. |
handler |
Async function that receives a RouteHandlerContext and returns a ResponseDto. |
security |
Optional per-route settings consumed by the relevant middleware. |
Use the same registry type for the builder and your route definitions when dispatching to application-specific controller tokens.
Match paths and methods
Section titled “Match paths and methods”The matcher compares both method and path. It ignores the query string when
matching, normalizes trailing slashes, and matches methods without case
sensitivity. For example, GET /api/orders/42?include=items matches
GET /api/orders/:id.
Both :id and {id} match one path segment. Static path matching is
case-sensitive. The first matching definition wins, including when a dynamic
route overlaps a static route. Register more specific routes first, for
example /api/orders/recent before /api/orders/:id, when both use the same
method.
If no route matches, the server returns 404. The matcher has a special
fallback for OPTIONS: if no explicit OPTIONS route matches but another
route matches the path, it supplies a preflight route. This fallback does not
replace middleware configuration; see Preflight requests.
Organize route lists
Section titled “Organize route lists”Group routes by feature and add each list before build():
import { XenoAppBuilder } from '@xeno-js/core'import type { OrdersRegistry } from './registry'import { orderRoutes } from './orders.routes'import { productRoutes } from './products.routes'
const builder = new XenoAppBuilder<OrdersRegistry>() .addRoutes(orderRoutes) .addRoutes(productRoutes)
// Register controllers, middleware, and a server integration as needed.const app = await builder.build()Each call appends definitions; it does not replace the earlier list. Registration order therefore matters across lists as well as within a list. Use route arrays typed with the application’s registry, and make sure that the registry includes all controller tokens used by those arrays.
Use the route handler
Section titled “Use the route handler”The handler receives a context with two members:
| Member | Purpose |
|---|---|
ctx.req |
A XenoRequest, extending the standard Request with path, optional query, and optional socket information. |
ctx.handle(token, input?) |
Dispatches to a registered controller and returns its ResponseDto. |
Call ctx.handle() with a token from the registry. The dispatcher runs the
configured middleware, resolves the controller in the active request scope,
and calls its handle() method. The token must resolve to a service with a
handle method at runtime. The generic dispatch signature checks the token
against the registry, but does not infer and validate the input and output
types from that controller’s contract. Validate input and keep those types
aligned explicitly.
The handler may instead return a ResponseDto directly. In that case it
bypasses ctx.handle() and its middleware and controller dispatch path.
Do not use a direct response as a substitute for configured authentication,
authorization, rate limiting, or other middleware checks.
When the Node context is used, request-scoped execution uses
AsyncLocalStorage to make RequestContext available to dispatched work.
See Context and Scopes for access to identity,
tracing, and scoped services.
Extract request input
Section titled “Extract request input”HttpRouteParamHelper.extractInput<TInput>(ctx.req, routePattern) combines
available body, query, and path values into one object. Pass the same pattern
used by the route so named path segments map to the intended keys.
For example:
const input = await HttpRouteParamHelper.extractInput<{ id: string include?: string}>(ctx.req, '/api/orders/:id')For /api/orders/42?include=items, the extracted values include
{ id: '42', include: 'items' }. This excerpt belongs inside a route handler
where ctx is available.
The helper applies these rules:
- For
POST,PUT, andPATCH, it reads JSON or URL-encoded bodies according toContent-Type; other content types are read as text. - Object body fields are merged into the input. Non-object values are placed
under
body. - Query values override body fields with the same name.
- Path parameters override both query and body fields with the same name.
The helper does not read a body for GET, DELETE, HEAD, or OPTIONS.
When no body value is available, the result can include body: undefined.
It decodes path parameters, strips control characters from extracted path
values and object body strings, and removes __proto__, constructor, and
prototype keys from object bodies. Query values come from ctx.req.query;
their parsing can depend on the server integration.
The type argument describes the expected shape; it does not validate, convert, or guarantee the values. In particular, do not assume a query value is a number without parsing and validating it. The helper also catches body read or parsing errors and proceeds without the parsed body. Use explicit validation before performing application operations; if a malformed body must produce a specific error, handle body parsing separately rather than relying on this helper to report the failure.
For separate extraction, extractPathParams(routePattern, requestPath) and
extractQueryParams(requestUrl) return string dictionaries. Invalid
percent-encoding in a path parameter can throw during decoding.
Configure route security
Section titled “Configure route security”Route security settings customize middleware behavior; they do not activate
middleware by themselves. Configure addMiddlewares() during bootstrap, then
apply policies to the relevant route definitions. Configure authentication
and authorization separately: CORS is not an authorization mechanism.
withCors() returns a route definition with allowed origins and a credentials
setting. It defaults withCredentials to true, so pass it explicitly when
appropriate:
import { withCors } from '@xeno-js/core'
// Reuse orderRoute and OrdersRegistry from the route definition.const publicOrderRoute = withCors<OrdersRegistry>( orderRoute, ['https://shop.example.com'], false,)Add publicOrderRoute to the builder instead of the original definition and
enable CORS through addMiddlewares(). A route’s allowOrigins and
withCredentials override the corresponding global values for that route.
For wildcard origins, use false for credentials unless you deliberately
configure a more restrictive credentialed policy. Review the allowed origins
for the deployment.
When you configure CSRF middleware, state-changing POST, PUT, PATCH, and
DELETE requests require its checks by default. Use withoutCsrf() only for a
route where disabling that protection is justified and another suitable
security boundary exists, such as a verified webhook:
import { withoutCsrf } from '@xeno-js/core'
// Reuse an application-defined, authenticated webhook route.const webhookRouteWithoutCsrf = withoutCsrf(webhookRoute)withCsrf() sets the route’s withCsrf policy to true; it does not enable
the middleware. In the current implementation, CSRF validation runs for the
state-changing methods listed above, not for GET even when its policy is
true. Configure the CSRF middleware and its secret before relying on these
policies.
Preflight requests
Section titled “Preflight requests”For a matching path without an explicit OPTIONS route, the matcher provides
a fallback route that dispatches the preflight request. Configure CORS and
enablePreflight in addMiddlewares() to use the preflight middleware.
It uses Access-Control-Request-Method to inspect the target route’s policy
and returns allowed methods and headers.
The origin check also runs for OPTIONS before the preflight middleware.
When using the fallback route, make sure the global allowed origins permit the
preflight origin as well as configuring the target route’s policy. Test the
actual browser preflight for each deployed transport and route. An unmatched
path does not receive the fallback route.
For controller implementation, registration, and response helpers, see HTTP Controllers. For a reference to request, route-policy, and HTTP utilities, see HTTP Helpers.
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
