Request Object (ExisRequest)
ExisJS wraps the standard Node.js IncomingMessage in an ExisRequest instance. It provides parsed properties, lazy body decoding, multipart upload helpers, IP resolution behind reverse proxies, and integration with the Web standard AbortSignal.
Core Request Properties
| Property | Type | Description |
|---|---|---|
req.params | TParams | Dynamic route parameters extracted from URL segments (e.g. /:id) |
req.query | TQuery | Parsed URL query parameters (e.g. ?page=1&limit=20) |
req.body | TBody | Parsed request body payload (JSON, URL-encoded, or multipart fields) |
req.headers | IncomingHttpHeaders | Incoming HTTP headers map (case-insensitive keys) |
req.cookies | Record<string, string> | Parsed request cookies dictionary |
req.file | ExisFile | First uploaded file from multipart form data |
req.files | ExisFile[] | All uploaded files from multipart form data |
req.ip | string | Resolved client IP address (evaluates trusted proxy headers) |
req.ips | string[] | Array of IP addresses when traversed through proxy hops |
req.hostname | string | Resolved hostname (from Host or X-Forwarded-Host) |
req.protocol | 'http' | 'https' | Request protocol |
req.secure | boolean | true when request is over HTTPS (req.protocol === 'https') |
req.method | string | HTTP method verb (GET, POST, PUT, etc.) |
req.path | string | Normalized pathname without query parameters |
req.originalUrl | string | Raw URL string including query parameters |
req.signal | AbortSignal | Standard AbortSignal triggered when client cancels connection |
req.fresh | boolean | true if client cache matches current ETag or Last-Modified |
req.stale | boolean | Inverse of req.fresh |
req.user | ExisUser | Authenticated user set by your auth middleware or guard. Type it once with declare module 'exisjs' { interface ExisUser { id: string; roles: string[]; permissions: string[] } } |
req.session | Record<string, any> | Session data store when session middleware is enabled |
req.log | Logger | Request-scoped Pino logger; every line carries requestId |
req.requestId | string | Unique request identifier header |
req.raw | IncomingMessage | Underlying Node.js IncomingMessage stream |
Accessing Request Data
In Functional Routes
In functional route handlers, req is available directly on the handler context alongside destructured convenience properties (body, query, params, headers):
import { controller, route } from 'exisjs/router'
export default controller({ updateUser: route.put('/:id', { handle: async ({ params, query, body, req }) => { const id = params.id const force = query.force === 'true' const clientIp = req.ip
return { id, force, clientIp, updated: body } }, }),})In Class-Based (OOP) Controllers
In OOP controllers, access req using the @Req() decorator or extract specific properties using @Param(), @Query(), @Body(), and @Headers():
import { Controller, Put, Param, Query, Body, Req } from 'exisjs/decorators'import type { Request } from 'exisjs/router'
@Controller()export default class UsersController { @Put('/:id') async updateUser( @Param('id') id: string, @Query('force') force: string, @Body() body: any, @Req() req: Request ) { const clientIp = req.ip return { id, force: force === 'true', clientIp, updated: body } }}On-Demand Body Parsing & Raw Streams
When dealing with webhooks (e.g. Stripe, GitHub), HMAC signature verification, or custom binary payloads, you can access the unparsed stream directly:
// 1. Read raw binary Buffer (ideal for webhook signature verification)const rawBuffer = await req.buffer()
// 2. Read raw UTF-8 stringconst rawText = await req.text()
// 3. Parse JSON body on demandconst data = await req.json<{ username: string }>()
// 4. Parse multipart/form-data or x-www-form-urlencodedconst { fields, files } = await req.formData()The body is read from the network once; these methods share the result. req.buffer() returns the bytes exactly as the client sent them, so binary payloads (images, protobuf, compressed or signed data) are safe. A body over bodyLimit makes these calls fail with 413 Payload Too Large.
Webhooks with @RawBody() (Class Controllers)
In class-based controllers, use the @RawBody() parameter decorator to inject the raw Buffer or decoded string directly:
import { Controller, Post, RawBody, Headers } from 'exisjs/decorators'
@Controller('/webhooks')export default class WebhookController { @Post('/stripe') async handleStripe( @RawBody() payload: Buffer, @Headers('stripe-signature') sig: string ) { return stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_SECRET!) }}Headers & Content Negotiation
ExisJS provides helper methods for header inspection and RFC content negotiation:
// Case-insensitive header retrievalconst auth = req.get('Authorization') // or req.header('authorization')
// Check Content-Typeif (req.is('json')) { // Content-Type contains application/json}
// Content negotiation - returns best supported matchconst format = req.accepts('json', 'html', 'text')const language = req.acceptsLanguages('en', 'es', 'de')Proxy & IP Address Resolution
When running behind reverse proxies, load balancers, or CDNs (such as AWS ALB, Cloudflare, NGINX, or Render), configure trustProxy in exis.config.ts:
import type { ExisConfig } from 'exisjs/config'
const config: ExisConfig = { // Exactly one proxy (e.g. NGINX or a load balancer) in front of the app: trustProxy: 1,}
export default configChoose the value that matches your deployment:
| Value | Trusts | Use when |
|---|---|---|
false (default) | Nothing; req.ip is the socket address | The app is reached directly |
N (number) | The N closest hops, counting the socket peer. With X-Forwarded-For: a, b and trustProxy: 1, req.ip is b, the address your proxy saw. Values a client prepends are ignored. | You run behind a known number of proxies |
true | Every hop, plus CDN headers (CF-Connecting-IP, True-Client-IP, X-Real-IP, X-Client-IP) | Every request passes through a CDN or proxy that overwrites these headers |
Warning: with trustProxy: true, any client that can reach the app directly can set these headers and spoof req.ip, and with it rate limits and IP filters. Prefer a hop count unless the app is only reachable through your proxy.
When enabled:
req.ipautomatically resolves the client IP, normalizes IPv6 loopback (::1$\to$127.0.0.1), and strips IPv4-mapped IPv6 prefixes (::ffff:192.168.1.1$\to$192.168.1.1).req.ipsprovides the complete upstream proxy chain.req.protocolresolves to'https'whenX-Forwarded-Proto: httpsis present.
Connection Cancellation & req.signal
ExisJS links req.signal to the lifecycle of the incoming HTTP connection. If a client closes their browser, navigates away, or aborts an HTTP request, req.signal fires an abort event.
Pass req.signal to downstream database queries or outgoing HTTP calls to avoid running orphaned operations:
import { controller, route } from 'exisjs/router'
export default controller({ exportReport: route.get('/', { handle: async ({ req }) => { // 1. Pass signal to fetch calls const upstream = await fetch('https://analytics.internal/data', { signal: req.signal, }) const data = await upstream.json()
// 2. Pass signal to database queries const report = await db.query('SELECT * FROM large_dataset', { signal: req.signal, })
return { data, report } }, }),})Request Timeouts
Dynamic timeout controls allow extending or resetting execution limits for long-running batch operations:
// Set active request timeout to 3 minutesreq.setTimeout(180000)
// Clear the active timeoutreq.clearTimeout()Custom Context Typing (TContext)
Middlewares can attach custom data to the request. You can strongly type this context either per-route or globally:
Per-Route Generic Context
import { controller, route } from 'exisjs/router'
interface AdminContext { adminUser: { id: string; role: 'superadmin' | 'moderator' }}
export default controller({ dashboard: route.get<unknown, Record<string, string>, Record<string, string>, AdminContext>('/', { handle: async ({ adminUser, req }) => { return { activeAdmin: adminUser.id } }, }),})App-Wide Context with createRouter
Define a typed router factory to eliminate type assertions across your entire project:
import { createRouter } from 'exisjs/router'
export interface AppContext { user: { id: string; email: string } tenantId: string}
export const { route, controller } = createRouter<AppContext>()Use your local router in route files to automatically infer injected context:
import { controller, route } from '../router'
export default controller({ getProfile: route.get('/', { handle: async ({ user, tenantId }) => { // user and tenantId are strongly typed without casting return { user, tenantId } }, }),})Next Steps
- Response Helpers & Streaming: Formatting JSON envelopes, custom headers, cookies, and SSE.
- Validation Guide: Building validation schemas with
tex. - Controllers: Parameter decorators and class-based controllers.
- Boundaries: Attaching authentication context via middleware cascades.