HTTP Engine
Every request your API receives arrives the same way underneath: a stream of raw bytes over a network connection, following the HTTP protocol. Something has to sit at the bottom of your server and do the unglamorous work of reading those bytes, figuring out what the caller is asking for, and sending bytes back. That piece of software is the HTTP engine, and it's the foundation every route you write, and every other page in this section, ultimately sits on top of.
You'll almost never touch this layer directly. You write @Get('/pets') and a function, and by the time your code runs, the request has already been parsed into something easy to work with. This page covers what that foundation actually is, what it hands you on every request, and a few server-level settings that live here rather than on any one route.
Two engines, one interface
Most Node.js HTTP frameworks are built directly on Node's own built-in HTTP handling. It's a reasonable, well-worn choice, but not the fastest one available, and a meaningful slice of your server's total throughput is decided before your code ever runs, purely by how efficiently the layer underneath it accepts connections and parses requests.
RapidREST runs on one of two engines instead:
- uWebSockets.js (uWS), a C++ networking library with Node.js bindings, built to be dramatically faster than Node's own HTTP handling. This is the default under Node.
- Bun's own built-in server, for projects running on the Bun runtime, which has its own from-scratch, high-performance HTTP implementation.
Which one runs is auto-detected at startup, Node gets uWS, Bun gets Bun's server, with nothing to configure in your own code either way. See Deployment → Bun if you want to run under Bun specifically.
What actually matters here isn't which engine is faster, it's that routing, cookies, sessions, auth, permissions, metrics, and error handling are each written exactly once, against a shared interface both engines implement, not duplicated per engine. Every route class and every page in this section works identically regardless of which one is running underneath. A few real implementation details do differ between them (how a URL gets matched to a route, how TLS certificate files get read), but none of it is visible from your code. You're not writing "the uWS version" of anything.
The request and response objects
Every route you write receives a consistent pair of objects, regardless of which engine produced them:
interface HttpRequest {
method: string; // "GET", "POST", etc.
path: string; // "/pets/123"
headers: Record<string, string | string[] | undefined>;
params: Record<string, string>; // values from the URL, like the "123" in "/pets/:id"
query: Record<string, string | string[]>; // ?key=value pairs
body: any; // the parsed request body, for POST/PUT/etc.
cookies: Record<string, string>;
session?: Record<string, any>; // see Sessions
user?: any; // set once someone is authenticated
}
interface HttpResponse {
statusCode: number;
status(code: number): this;
setHeader(key: string, value: string | number | string[]): this; // replaces any existing value(s)
appendHeader(key: string, value: string | number): this; // adds a value without clobbering
getHeader(key: string): string | string[] | undefined;
json(data: any): void;
send(data?: any): void;
end(data?: any): void;
onFinish(handler: () => void | Promise<void>): void; // fires once, when the response completes
}
setHeader() versus appendHeader() matters the moment more than one thing might want to set the same header, Set-Cookie being the common case: see Cookies & CORS. @Request/@Response are the decorators that hand these objects to your route functions, covered properly in Request Parameters. The rest of this section covers what's built into this layer: Error Handling, Sessions, Cookies & CORS, Streaming Responses, WebSockets, and Global Middleware.
Basic server settings
A small number of settings control the server itself rather than any one route, set under Configuration:
| Key | Default | What it controls |
|---|---|---|
port | 3000 | Which port your server listens on |
listen_host | 0.0.0.0 | Which network interfaces to accept connections on. 0.0.0.0 means "any" |
ssl.key / ssl.cert / ssl.ca / ssl.passphrase | none | File paths that turn on HTTPS, if you're terminating TLS in the app itself rather than in front of it |
max_body_size | 10485760 (10 MiB) | The largest request body accepted before the request is rejected with 413 Payload Too Large |
conf.defaults({
port: 3000,
listen_host: '0.0.0.0',
ssl: {
key: './certs/server.key',
cert: './certs/server.crt',
},
max_body_size: 10 * 1024 * 1024,
});
Client IP and trusted proxies
Behind a load balancer or reverse proxy, the socket's own address is the proxy's, not the caller's, the real client IP shows up in a header like X-Forwarded-For instead. RapidREST won't trust that header by default, since anyone can set it on a direct request, it's only consulted when the connecting socket's address is itself in your configured trusted_proxies list:
conf.defaults({
trusted_proxies: ['10.0.0.0/8'],
});
This is what a handful of framework internals (audit-log IP capture on create/update, session IP checks) use to resolve the real caller. Leave it unset and RapidREST always uses the raw socket address, which is the safe default when you aren't behind a proxy.
Shutting down
A RapidREST server shuts down in order, not all at once: background services stop first, then the listen socket closes (no new connections accepted, in-flight ones finish), then database connections close. If any step hangs, a 30-second watchdog forces the process to exit rather than leaving it stuck. This runs automatically on SIGINT/SIGTERM, there's nothing you need to wire up yourself for a normal deployment.
If you're curious exactly how much the engine choice affects real request throughput, see Performance for numbers comparing RapidREST against Express, Fastify, and Next.js (measured on the uWS engine, there's no published Bun comparison yet).