ExisJS

Requests

ExisJS wraps the native Node.js IncomingMessage object into an ExisRequest instance. It abstracts away the complexities of stream parsing, multipart form processing, and header normalization, giving you a typed interface.


1. Body, Query, and Params

The most common request properties are automatically parsed and strongly typed (especially when using the Schema Validation middleware).

  • req.body: The parsed body of the request (JSON, URL-encoded, or Multipart).
  • req.query: An object containing the URL query string parameters (e.g., ?id=123).
  • req.params: An object containing route parameters (e.g., /:id).

Functional Paradigm

src/http/route.ts
export default controller({  updateUser: route.post('/users/:id', {    handle(req, res) {      const id = req.params.id          // "123"      const force = req.query.force     // "true"      const payload = req.body          // { email: "[email protected]" }    }  })})

Class-Based (OOP) Paradigm

src/http/route.ts
import { Controller, Post, Param, Query, Body } from 'exisjs/decorators'
@Controller()export default class UsersController {  @Post('/users/:id')  async updateUser(    @Param('id') id: string,    @Query('force') force: string,    @Body() payload: any  ) {    // Variables are automatically injected  }}

2. On-Demand Parsing

To maximize performance, ExisJS does not parse the request body immediately for every route. If you aren't using the Validation middleware, you can manually trigger parsing on-demand.

Functional Paradigm

export default controller({  processData: route.post('/process', {    async handle(req, res) {      // Parses application/json      const data = await req.json<{ key: string }>()            // Parses raw text (text/plain)      const text = await req.text()    }  })})

Class-Based (OOP) Paradigm

src/http/route.ts
import { Controller, Post, Req } from 'exisjs/decorators'
@Controller()export default class ProcessController {  @Post('/process')  async processData(@Req() req: any) {    // Parses application/json    const data = await req.json<{ key: string }>()        // Parses raw text (text/plain)    const text = await req.text()  }}

3. File Uploads & Form Data

Handling multipart/form-data in Node.js can be difficult. ExisJS provides a stream parser out-of-the-box.

Calling req.formData() returns both the standard text fields and the uploaded files. ExisJS also attaches a .saveToDisk() helper to every uploaded file, which automatically handles writing the buffer and generating collision-free filenames.

Functional Paradigm

src/http/upload/route.ts
import { controller, route } from 'exisjs/router'
export default controller({  uploadAvatar: route.post('/avatar', {    async handle(req, res) {      // 1. Parse the multipart form data      const { fields, files } = await req.formData()            // 2. Access the file (e.g., from an input named "profile_pic")      const uploadedFile = files['profile_pic']            if (uploadedFile) {        // 3. Save it to disk safely. Returns the absolute saved path.        const savedPath = await uploadedFile.saveToDisk('./uploads/avatars')        return { success: true, path: savedPath }      }            return { success: false, error: "No file uploaded" }    }  })})

Class-Based (OOP) Paradigm

src/http/upload/route.ts
import { Controller, Post, UploadedFile } from 'exisjs/decorators'
@Controller()export default class UploadController {  @Post('/avatar')  async uploadAvatar(@UploadedFile('profile_pic') profilePic: any) {    if (profilePic) {      // Save it to disk safely. Returns the absolute saved path.      const savedPath = await profilePic.saveToDisk('./uploads/avatars')      return { success: true, path: savedPath }    }        return { success: false, error: "No file uploaded" }  }}

Streaming (streamUpload)

If you are dealing with large files and don't want to buffer anything in memory, ExisJS provides a direct streaming helper. It pipes the multipart chunks directly to the disk in real-time.

Functional Paradigm

import { controller, route } from 'exisjs/router'
export default controller({  uploadCover: route.post('/cover', {    async handle(req, res) {      // 1. Using the method on the request object      // const { fields, files } = await req.streamUpload('./uploads/covers')
      // 2. Or using the exported helper from exisjs/storage      const { streamUpload } = await import('exisjs/storage')      const { fields, files } = await streamUpload(req, { dest: './uploads/covers' })            return {         success: true,         uploadedFiles: files.map(f => f.filename),        customFields: fields       }    }  })})

Class-Based (OOP) Paradigm

import { Controller, Post, Req } from 'exisjs/decorators'import { streamUpload } from 'exisjs/storage'
@Controller()export default class UploadController {  @Post('/cover')  async uploadCover(@Req() req: any) {    // Both req.streamUpload() and the exported helper from exisjs/storage work.    const { fields, files } = await streamUpload(req, { dest: './uploads/covers' })        return {       success: true,       uploadedFiles: files.map(f => f.filename),      customFields: fields     }  }}

4. HTTP Headers & Content Negotiation

// Get a specific header (case-insensitive)const authHeader = req.get('Authorization')// orconst authHeader = req.header('authorization')
// Check the Content-Typeif (req.is('application/json')) { ... }
// Content Negotiation (returns the best match)const format = req.accepts('html', 'json', 'text')const language = req.acceptsLanguages('en', 'es', 'fr')

5. Network Information

ExisJS handles trust-proxies. If you are behind Cloudflare, Nginx, or an AWS Load Balancer, simply set trustProxy: true in your exis() config, and these properties will automatically resolve the true client IP from X-Forwarded-For.

  • req.ip: The client's IP address.
  • req.ips: An array of IPs if multiple proxies were traversed.
  • req.hostname: The resolved hostname (from Host or X-Forwarded-Host).
  • req.protocol: Returns 'http' or 'https'.
  • req.secure: A boolean (true if req.protocol === 'https').

6. Context & Metadata

As your request moves through ExisJS Middlewares, various properties are attached to req for your convenience:

  • req.log: A request-scoped instance of the Pino logger (via requestLogger).
  • req.requestId: The unique Trace ID (via requestId).
  • req.user: The authenticated user payload (via JWT auth middleware).
  • req.session: The stateful session object (via session middleware).
  • req.fresh: Returns true if the client's cache is still valid (ETag match), meaning you can send a 304 response.

Inline Context Typing (Per-Route)

If you don't want to create a globally typed router with createRouter, ExisJS natively types req.user as any by default so you never get annoying compiler errors (e.g. req.user.userId just works!).

However, if you want strict type safety on a specific route without global configuration, you can pass your custom context directly to the route's 4th generic parameter:

import { route } from "exisjs/router";
export default route.get<any, any, any, { user: { userId: string } }>("/", {  async handle({ req }) {    // Typed. No "as any" needed.    const id = req.user.userId;   }})

Strongly-Typed Context & Native Destructuring (App-Wide)

To provide a better developer experience, ExisJS allows you to define a custom typed context for your application. This completely eliminates the need for global .d.ts augmentations and as any casts.

To do this, create a custom router factory (e.g., src/http/router.ts) and define the exact shape of your application's context (like the user object, workspace, tenant ID, etc).

src/http/router.ts
import { createRouter } from "exisjs/router";
// 1. Define everything your middlewares will attach to the requestinterface AppContext {  user: { id: string; role: string };  workspace: string;}
// 2. Export a custom typed route and controllerexport const { route, controller } = createRouter<AppContext>();

Now, instead of importing from exisjs/router, import from your local router. Your handlers can natively destructure your custom properties.

src/http/users/route.ts
import { route, controller } from "../../router.ts";
export default controller({  getProfile: route.get("/", {    async handle({ user, workspace }) {      // Strongly typed.       // No more `req.user`, no more `as any`.      console.log(user.role, workspace);    }  })});
WARNING

Important Middleware Rule: In order for destructuring to work (e.g. handle({ user })), the user property MUST be populated before the handler executes. This means you must populate it inside a middleware instead of calling an inline function inside the handler.