Core Concepts

Response Helpers API

Complete reference for every response helper in MoroJS. Three layers of helpers give you full control, from one-liner convenience methods to fluent builders for complex scenarios.

Overview

MoroJS provides three layers of response helpers. Pick the one that fits your needs. They all produce the same consistent response shape.

LayerSets HTTP Status?Sends Response?Import
res.* Yes YesAvailable on every res object
response.* No Noimport { response } from '@morojs/moro'
ResponseBuilder No Noimport { ResponseBuilder } from '@morojs/moro'
Success Shape
typescript
{
  success: true,
  data: T,
  message?: string
}
Error Shape
typescript
{
  success: false,
  error: string,
  code?: string,
  message?: string
}

Types

All handler signatures use HttpRequest and HttpResponse from @morojs/moro.

Import
import { HttpRequest, HttpResponse } from '@morojs/moro';

HttpRequest

Extends Node's IncomingMessage with parsed properties:

HttpRequest Interface
1interface HttpRequest extends IncomingMessage {
2  params: Record<string, string>;
3  query: Record<string, string>;
4  body: any;
5  path: string;
6  headers: Record<string, string>;
7  ip: string;
8  requestId: string;
9  cookies?: Record<string, string>;
10  files?: Record<string, any>;
11  [key: string]: any;
12}

HttpResponse

Extends Node's ServerResponse with all the methods documented below:

HttpResponse Type
type HttpResponse = ServerResponse & MoroResponseMethods;

Handler Signatures

Handler & Middleware Types
1type HttpHandler = (
2  req: HttpRequest,
3  res: HttpResponse
4) => Promise<void> | void;
5
6type Middleware = (
7  req: HttpRequest,
8  res: HttpResponse,
9  next: () => void
10) => Promise<void> | void;

CookieOptions

CookieOptions Interface
1interface CookieOptions {
2  maxAge?: number;
3  expires?: Date;
4  httpOnly?: boolean;
5  secure?: boolean;
6  sameSite?: 'strict' | 'lax' | 'none';
7  domain?: string;
8  path?: string;
9  critical?: boolean;
10  throwOnLateSet?: boolean;
11}

ResponseState

ResponseState Interface
1interface ResponseState {
2  headersSent: boolean;
3  statusCode: number;
4  headers: Record<string, any>;
5  finished: boolean;
6  writable: boolean;
7}

Core Methods

Foundational methods on every res object for sending data, setting status codes, managing cookies, and more.

res.json(data)

Serializes data as JSON and sends it. Sets Content-Type: application/json.

Usage
1handler: (req, res) => {
2  res.json({ hello: 'world' });
3}

res.status(code)

Sets the HTTP status code. Returns res for chaining.

Usage
1handler: (req, res) => {
2  res.status(201).json({ success: true, data: newUser });
3}

res.send(data)

Sends a string or Buffer response.

Usage
1handler: (req, res) => {
2  res.send('Hello, world');
3}

res.cookie(name, value, options?)

Sets a cookie on the response. Returns res for chaining. See CookieOptions above for available options.

Usage
1handler: (req, res) => {
2  res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'strict' })
3     .json({ success: true });
4}

res.clearCookie(name, options?)

Clears a cookie. Returns res for chaining.

Usage
1handler: (req, res) => {
2  res.clearCookie('session').json({ success: true });
3}

res.redirect(url, status?)

Redirects to the given URL. Defaults to 302.

Usage
1handler: (req, res) => {
2  res.redirect('/login');
3  // or with a specific status
4  res.redirect('/new-location', 301);
5}

res.sendFile(filePath)

Streams a file as the response. Returns a Promise<void>.

Usage
1handler: async (req, res) => {
2  await res.sendFile('/uploads/report.pdf');
3}

res.render(template, data?)

Renders a template (if a template engine is configured). Returns a Promise<void>.

Usage
1handler: async (req, res) => {
2  await res.render('dashboard', { user: req.user });
3}

Success & Error Responses

These methods set the HTTP status code and send the response in a single call. After calling any of these, the response is finished.

Success Responses

res.success(data, message?)

200 OK

Usage
1handler: (req, res) => {
2  const users = await getUsers();
3  res.success(users);
4}
5
6// With message
7handler: (req, res) => {
8  const user = await createUser(req.body);
9  res.success(user, 'User created successfully');
10}
Response Body
1{
2  "success": true,
3  "data": { "id": 1, "name": "Alice" },
4  "message": "User created successfully"
5}

res.created(data, location?)

201 Created

Optionally sets the Location header.

Usage
1handler: (req, res) => {
2  const user = await createUser(req.body);
3  res.created(user, `/api/users/${user.id}`);
4}
Response Body
1{ "success": true, "data": { "id": 1, "name": "Alice" } }
2
3// Headers: Location: /api/users/1 (when provided)

res.noContent()

204 No Content

Sends an empty body. Perfect for delete operations.

Usage
1handler: (req, res) => {
2  await deleteUser(req.params.id);
3  res.noContent();
4}

res.paginated(data, pagination)

200 OK

Pagination metadata (totalPages, hasNext, hasPrev) is calculated automatically.

ParameterType
dataT[]
pagination{ page: number; limit: number; total: number }
Usage
1handler: (req, res) => {
2  const { users, total } = await getUsers({ page: 1, limit: 20 });
3  res.paginated(users, { page: 1, limit: 20, total });
4}
Response Body
1{
2  "success": true,
3  "data": [{ "id": 1, "name": "Alice" }],
4  "pagination": {
5    "page": 1,
6    "limit": 20,
7    "total": 100,
8    "totalPages": 5,
9    "hasNext": true,
10    "hasPrev": false
11  }
12}

Error Responses

res.error(error, code?, message?)

200

Sends a 200 OK response (does not set a non-200 status code) with an error body. Use res.status() beforehand if you need a specific HTTP code, or use one of the specific error helpers below.

Usage
1handler: (req, res) => {
2  res.status(500).json({ success: false, error: 'Something went wrong' });
3  // or
4  res.error('Something went wrong', 'CUSTOM_ERROR', 'Additional details');
5}

res.badRequest(message?)

400
Default message: 'Invalid request'
Usage
1handler: (req, res) => {
2  if (!req.body.email) {
3    res.badRequest('Email is required');
4    return;
5  }
6}
Response Body
1{
2  "success": false,
3  "error": "Bad Request",
4  "code": "BAD_REQUEST",
5  "message": "Email is required"
6}

res.unauthorized(message?)

401
Default message: 'Authentication required'
Usage
1handler: (req, res) => {
2  if (!req.user) {
3    res.unauthorized();
4    return;
5  }
6}
Response Body
1{
2  "success": false,
3  "error": "Unauthorized",
4  "code": "UNAUTHORIZED",
5  "message": "Authentication required"
6}

res.forbidden(message?)

403
Default message: 'Insufficient permissions'
Usage
1handler: (req, res) => {
2  if (!req.user.roles.includes('admin')) {
3    res.forbidden('Admin access required');
4    return;
5  }
6}
Response Body
1{
2  "success": false,
3  "error": "Forbidden",
4  "code": "FORBIDDEN",
5  "message": "Admin access required"
6}

res.notFound(resource?)

404
Default resource: 'Resource'
Usage
1handler: (req, res) => {
2  const user = await getUser(req.params.id);
3  if (!user) {
4    res.notFound('User');
5    return;
6  }
7}
Response Body
1{
2  "success": false,
3  "error": "Not Found",
4  "code": "NOT_FOUND",
5  "message": "User not found"
6}

res.conflict(message)

409
Message is required
Usage
1handler: (req, res) => {
2  const existing = await getUserByEmail(req.body.email);
3  if (existing) {
4    res.conflict('Email already in use');
5    return;
6  }
7}
Response Body
1{
2  "success": false,
3  "error": "Conflict",
4  "code": "CONFLICT",
5  "message": "Email already in use"
6}

res.validationError(errors)

422

Sends field-level error details. Each error has field, message, and optional code.

Usage
1handler: (req, res) => {
2  const errors = validateUser(req.body);
3  if (errors.length > 0) {
4    res.validationError([
5      { field: 'email', message: 'Invalid email format', code: 'INVALID_FORMAT' },
6      { field: 'age', message: 'Must be at least 18' },
7    ]);
8    return;
9  }
10}
Response Body
1{
2  "success": false,
3  "error": "Validation Failed",
4  "code": "VALIDATION_ERROR",
5  "errors": [
6    { "field": "email", "message": "Invalid email format", "code": "INVALID_FORMAT" },
7    { "field": "age", "message": "Must be at least 18" }
8  ]
9}

res.rateLimited(retryAfter?)

429

When retryAfter (seconds) is provided, the Retry-After header is set automatically.

Usage
1handler: (req, res) => {
2  const limited = await checkRateLimit(req.ip);
3  if (limited) {
4    res.rateLimited(60);
5    return;
6  }
7}
Response Body
1{
2  "success": false,
3  "error": "Rate Limit Exceeded",
4  "code": "RATE_LIMITED",
5  "message": "Too many requests. Retry after 60 seconds.",
6  "retryAfter": 60
7}
8
9// Headers: Retry-After: 60

res.internalError(message?)

500
Default message: 'Internal server error'
Usage
1handler: (req, res) => {
2  try {
3    const data = await fetchExternalService();
4    res.success(data);
5  } catch (err) {
6    res.internalError('Failed to fetch data from external service');
7  }
8}
Response Body
1{
2  "success": false,
3  "error": "Internal Server Error",
4  "code": "INTERNAL_ERROR",
5  "message": "Failed to fetch data from external service"
6}

Header & State Utilities

Low-level utilities for inspecting and manipulating headers and response state.

res.hasHeader(name)

Returns true if the given header has been set.

Usage
1if (!res.hasHeader('X-Request-Id')) {
2  res.setHeader('X-Request-Id', req.requestId);
3}

res.setBulkHeaders(headers)

Sets multiple headers at once. Returns res for chaining.

Usage
1res.setBulkHeaders({
2  'X-Request-Id': req.requestId,
3  'X-Response-Time': 42,
4  'Cache-Control': 'no-store',
5}).json(data);

res.appendHeader(name, value)

Appends a value to an existing header (or creates it). Returns res for chaining.

Usage
1res.appendHeader('Set-Cookie', 'a=1')
2   .appendHeader('Set-Cookie', 'b=2');

res.canSetHeaders()

Returns true if headers have not been sent yet and can still be modified.

Usage
1if (res.canSetHeaders()) {
2  res.setHeader('X-Custom', 'value');
3}

res.getResponseState()

Returns the current ResponseState object.

Usage
1const state = res.getResponseState();
2// { headersSent: false, statusCode: 200, headers: {...}, finished: false, writable: true }

Standalone Helper Functions

These functions build response body objects only. They do not set HTTP status codes or send the response. Use them with res.status().json() for full control, or return them from a handler (which sends 200 by default).

Import
import { response } from '@morojs/moro';

Quick Reference

FunctionReturns
response.success(data, message?){ success: true, data, message? }
response.error(error, code?, message?){ success: false, error, code?, message? }
response.validationError(details, message?){ success: false, error: "Validation failed", ... }
response.unauthorized(message?){ success: false, error: "Unauthorized", ... }
response.forbidden(message?){ success: false, error: "Forbidden", ... }
response.notFound(resource?){ success: false, error: "Not Found", ... }
response.conflict(message){ success: false, error: "Conflict", ... }
response.badRequest(message?){ success: false, error: "Bad Request", ... }
response.internalError(message?){ success: false, error: "Internal Server Error", ... }
response.rateLimited(retryAfter?){ success: false, error: "Too Many Requests", ... }
Return directly (sends 200 with body)
1import { response } from '@morojs/moro';
2
3handler: (req, res) => {
4  const users = await getUsers();
5  return response.success(users);
6}
Pair with res.status() for proper HTTP codes
1import { response } from '@morojs/moro';
2
3handler: (req, res) => {
4  const user = await getUser(req.params.id);
5  if (!user) {
6    return res.status(404).json(response.notFound('User'));
7  }
8  return response.success(user);
9}

ResponseBuilder

A fluent API for building complex responses. Returns body objects only (no HTTP status or send). Use method chaining to compose responses step by step.

Import
import { ResponseBuilder } from '@morojs/moro';

Builder Methods

MethodDescription
ResponseBuilder.success(data)Start a success response
ResponseBuilder.error(error, code?)Start an error response
.message(msg)Add a message
.details(details)Add details (error responses)
.code(code)Add an error code
.build()Return the final response object
Success with ResponseBuilder
1handler: (req, res) => {
2  const users = await getUsers();
3  return ResponseBuilder.success(users)
4    .message('Retrieved all active users')
5    .build();
6}
Error with ResponseBuilder
1handler: (req, res) => {
2  return res.status(404).json(
3    ResponseBuilder.error('User not found', 'USER_NOT_FOUND')
4      .details({ id: req.params.id, searched: ['db', 'cache'] })
5      .build()
6  );
7}

Important: Return Values vs HTTP Status Codes

When a handler returns a plain object, the framework serializes it as JSON with a 200 OK status. A status field in the JSON body does not affect the HTTP status code.

The Pitfall: This sends HTTP 200, NOT 403
1// THIS SENDS HTTP 200 -- the "status" field is just data in the JSON body
2handler: (req, res) => {
3  return { error: 'Forbidden', status: 403 };
4}

To send an actual HTTP 403, use one of these:

Option 1: res.* helper (Recommended)

typescript
1handler: (req, res) => {
2  res.forbidden('You do not have access');
3  return;
4}

Option 2: res.status().json() with standalone helper

typescript
1handler: (req, res) => {
2  return res.status(403).json(response.forbidden('You do not have access'));
3}

Option 3: res.status().json() manually

typescript
1handler: (req, res) => {
2  return res.status(403).json({ success: false, error: 'Forbidden' });
3}

Rule of thumb: If you need an HTTP status other than 200, always use a res.* method or res.status() explicitly.

Quick Reference: All res.* Methods

Core

  • res.json(data)
  • res.status(code)
  • res.send(data)
  • res.cookie(name, val, opts?)
  • res.clearCookie(name, opts?)
  • res.redirect(url, status?)
  • res.sendFile(path)
  • res.render(tpl, data?)

Success

  • res.success(data, msg?)200
  • res.created(data, loc?)201
  • res.noContent()204
  • res.paginated(data, pg)200

Utilities

  • res.hasHeader(name)
  • res.setBulkHeaders(hdrs)
  • res.appendHeader(name, val)
  • res.canSetHeaders()
  • res.getResponseState()

Errors

  • res.error(err, code?, msg?)200
  • res.badRequest(msg?)400
  • res.unauthorized(msg?)401
  • res.forbidden(msg?)403
  • res.notFound(resource?)404
  • res.conflict(msg)409
  • res.validationError(errs)422
  • res.rateLimited(retry?)429
  • res.internalError(msg?)500

Related