ExisJS

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

PropertyTypeDescription
req.paramsTParamsDynamic route parameters extracted from URL segments (e.g. /:id)
req.queryTQueryParsed URL query parameters (e.g. ?page=1&limit=20)
req.bodyTBodyParsed request body payload (JSON, URL-encoded, or multipart fields)
req.headersIncomingHttpHeadersIncoming HTTP headers map (case-insensitive keys)
req.cookiesRecord<string, string>Parsed request cookies dictionary
req.fileExisFileFirst uploaded file from multipart form data
req.filesExisFile[]All uploaded files from multipart form data
req.ipstringResolved client IP address (evaluates trusted proxy headers)
req.ipsstring[]Array of IP addresses when traversed through proxy hops
req.hostnamestringResolved hostname (from Host or X-Forwarded-Host)
req.protocol'http' | 'https'Request protocol
req.securebooleantrue when request is over HTTPS (req.protocol === 'https')
req.methodstringHTTP method verb (GET, POST, PUT, etc.)
req.pathstringNormalized pathname without query parameters
req.originalUrlstringRaw URL string including query parameters
req.signalAbortSignalStandard AbortSignal triggered when client cancels connection
req.freshbooleantrue if client cache matches current ETag or Last-Modified
req.stalebooleanInverse of req.fresh
req.userExisUserAuthenticated user set by your auth middleware or guard. Type it once with declare module 'exisjs' { interface ExisUser { id: string; roles: string[]; permissions: string[] } }
req.sessionRecord<string, any>Session data store when session middleware is enabled
req.logLoggerRequest-scoped Pino logger; every line carries requestId
req.requestIdstringUnique request identifier header
req.rawIncomingMessageUnderlying 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):

src/http/users/route.ts
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():

src/http/users/route.ts
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:

src/http/webhooks/route.ts
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:

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 config

Choose the value that matches your deployment:

ValueTrustsUse when
false (default)Nothing; req.ip is the socket addressThe 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
trueEvery 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
DEFAULT

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.ip automatically 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.ips provides the complete upstream proxy chain.
  • req.protocol resolves to 'https' when X-Forwarded-Proto: https is 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:

src/http/reports/route.ts
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

src/http/admin/route.ts
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:

src/http/router.ts
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:

src/http/profile/route.ts
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