Responses
Unlike standard Node.js ServerResponse objects which are notoriously low-level and cumbersome, ExisJS wraps every response in a highly optimized ExisResponse class. It provides an expressive, chainable API while maintaining the raw performance of native Node.
While ExisJS routes allow you to simply return { data: ... } (which automatically serializes to JSON), the res object gives you granular control over headers, cookies, streams, and file downloads.
1. Returning Data vs Sending Data
The most common way to respond to a request in ExisJS is to simply return an object or array. ExisJS intercepts the return value, automatically serializes it to JSON, sets the Content-Type, calculates the Content-Length, and sends it down the wire.
Functional Paradigm
import { controller, route } from 'exisjs/router'
export default controller({ getUsers: route.get('/', { handle(req, res) { // ExisJS automatically handles JSON serialization! return [{ id: 1, name: "Alice" }] } })})Class-Based (OOP) Paradigm
import { Controller, Get } from 'exisjs/decorators'
@Controller()export default class UsersController { @Get('/') async getUsers() { // ExisJS automatically handles JSON serialization! return [{ id: 1, name: "Alice" }] }}However, you can completely bypass this and use the res object manually:
Functional Paradigm
export default controller({ getUsers: route.get('/', { handle(req, res) { // Chainable response API res.status(201).json([{ id: 1, name: "Alice" }]) } })})Class-Based (OOP) Paradigm
import { Controller, Get, Res } from 'exisjs/decorators'
@Controller()export default class UsersController { @Get('/') async getUsers(@Res() res: any) { // Chainable response API res.status(201).json([{ id: 1, name: "Alice" }]) }}2. Standardized JSON Responses (exisjs/response)
To keep your APIs consistent, ExisJS exports standard success() and error() builders.
Using these ensures your entire application adheres to a unified { success: boolean, data?: any, error?: any } structure, which frontend teams love.
Functional Paradigm
import { controller, route } from 'exisjs/router'import { success, error } from 'exisjs/response'
export default controller({ getUser: route.get('/:id', { handle(req, res) { const user = db.users.find(req.params.id) if (!user) { res.status(404) // Generates: { success: false, error: { message: "User not found", code: "NOT_FOUND" } } return error("User not found", "NOT_FOUND") } // Generates: { success: true, data: { id: 1, name: "Alice" }, message: "Found user" } return success(user, "Found user") } })})Class-Based (OOP) Paradigm
import { Controller, Get, Param, Res } from 'exisjs/decorators'import { success, error } from 'exisjs/response'
@Controller()export default class UsersController { @Get('/:id') async getUser(@Param('id') id: string, @Res() res: any) { const user = db.users.find(id) if (!user) { res.status(404) // Generates: { success: false, error: { message: "User not found", code: "NOT_FOUND" } } return error("User not found", "NOT_FOUND") } // Generates: { success: true, data: { id: 1, name: "Alice" }, message: "Found user" } return success(user, "Found user") }}3. Setting Headers & Status
The ExisResponse object implements a beautiful, chainable API for HTTP headers and status codes.
res.status(404) .set('X-Custom-Header', 'exis-is-awesome') .append('Set-Cookie', 'foo=bar') .json({ error: 'Not Found' })Supported Header Methods
res.status(code): Sets the HTTP status code.res.set(key, value)(orres.header()): Sets an HTTP header.res.append(key, value): Appends to an existing header (e.g., multipleSet-Cookiedirectives).res.removeHeader(key): Deletes a header.res.type(mimeType): A shorthand for setting theContent-Typeheader (e.g.,res.type('html')becomestext/html).res.vary(field): Adds a field to theVaryheader for caching.
3. Cookies
ExisJS provides native helper methods for setting and clearing cookies safely.
// Set a secure, HTTP-only cookieres.cookie('session_id', '123456', { maxAge: 86400, // 1 day in seconds httpOnly: true, secure: true, sameSite: 'Strict'})
// Clear a cookieres.clearCookie('session_id')4. File Downloads & Streaming
ExisJS makes streaming files incredibly simple and efficient, fully bypassing V8 memory bottlenecks by piping native Node streams directly to the socket.
res.download()
Automatically reads a file from disk, sets the Content-Disposition header to force a download in the browser, and infers the correct MIME type based on the file extension.
Functional Paradigm
import { controller, route } from 'exisjs/router'
export default controller({ downloadReport: route.get('/report', { handle(req, res) { res.download('/absolute/path/to/report.pdf', 'Q4-Report.pdf') } })})Class-Based (OOP) Paradigm
import { Controller, Get, Res } from 'exisjs/decorators'
@Controller()export default class ReportController { @Get('/report') async downloadReport(@Res() res: any) { res.download('/absolute/path/to/report.pdf', 'Q4-Report.pdf') }}res.sendStream()
If you are fetching data from an external source (like AWS S3) or generating data on the fly, you can pipe any ReadableStream directly to the client.
Functional Paradigm
import { controller, route } from 'exisjs/router'import fs from 'node:fs'
export default controller({ streamVideo: route.get('/video', { handle(req, res) { const videoStream = fs.createReadStream('/path/to/video.mp4') res.type('video/mp4') res.sendStream(videoStream) } })})Class-Based (OOP) Paradigm
import { Controller, Get, Res } from 'exisjs/decorators'import fs from 'node:fs'
@Controller()export default class VideoController { @Get('/video') async streamVideo(@Res() res: any) { const videoStream = fs.createReadStream('/path/to/video.mp4') res.type('video/mp4') res.sendStream(videoStream) }}5. Redirects
You can instantly redirect users to another URL using res.redirect(). It defaults to a 302 Found temporary redirect, but you can override the status code.
// Temporary Redirect (302)res.redirect('/login')
// Permanent Redirect (301)res.redirect('https://google.com', 301)6. ETags & Conditional Requests (304 Not Modified)
ExisJS features native, automatic ETag generation.
If the client sends an If-None-Match header matching the ETag of the current response, ExisJS will immediately abort serialization and send a lightning-fast 304 Not Modified header with an empty body, saving significant bandwidth and CPU cycles.
Note: ETag generation is disabled by default to maximize raw throughput, but can be enabled globally in your exis() config, or manually per-route via res.etagEnabled = true.