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
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
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
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
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
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 (fromHostorX-Forwarded-Host).req.protocol: Returns'http'or'https'.req.secure: A boolean (true ifreq.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 (viarequestLogger).req.requestId: The unique Trace ID (viarequestId).req.user: The authenticated user payload (viaJWTauth middleware).req.session: The stateful session object (viasessionmiddleware).req.fresh: Returnstrueif 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).
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.
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); } })});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.