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.
| Layer | Sets HTTP Status? | Sends Response? | Import |
|---|---|---|---|
| res.* | Yes | Yes | Available on every res object |
| response.* | No | No | import { response } from '@morojs/moro' |
| ResponseBuilder | No | No | import { ResponseBuilder } from '@morojs/moro' |
{
success: true,
data: T,
message?: string
}{
success: false,
error: string,
code?: string,
message?: string
}Types
All handler signatures use HttpRequest and HttpResponse from @morojs/moro.
import { HttpRequest, HttpResponse } from '@morojs/moro';HttpRequest
Extends Node's IncomingMessage with parsed properties:
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:
type HttpResponse = ServerResponse & MoroResponseMethods;Handler Signatures
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
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
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.
1handler: (req, res) => {
2 res.json({ hello: 'world' });
3}res.status(code)
Sets the HTTP status code. Returns res for chaining.
1handler: (req, res) => {
2 res.status(201).json({ success: true, data: newUser });
3}res.send(data)
Sends a string or Buffer response.
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.
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.
1handler: (req, res) => {
2 res.clearCookie('session').json({ success: true });
3}res.redirect(url, status?)
Redirects to the given URL. Defaults to 302.
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>.
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>.
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
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}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.
1handler: (req, res) => {
2 const user = await createUser(req.body);
3 res.created(user, `/api/users/${user.id}`);
4}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.
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.
| Parameter | Type |
|---|---|
| data | T[] |
| pagination | { page: number; limit: number; total: number } |
1handler: (req, res) => {
2 const { users, total } = await getUsers({ page: 1, limit: 20 });
3 res.paginated(users, { page: 1, limit: 20, total });
4}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?)
200Sends 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.
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'Invalid request'1handler: (req, res) => {
2 if (!req.body.email) {
3 res.badRequest('Email is required');
4 return;
5 }
6}1{
2 "success": false,
3 "error": "Bad Request",
4 "code": "BAD_REQUEST",
5 "message": "Email is required"
6}res.unauthorized(message?)
401'Authentication required'1handler: (req, res) => {
2 if (!req.user) {
3 res.unauthorized();
4 return;
5 }
6}1{
2 "success": false,
3 "error": "Unauthorized",
4 "code": "UNAUTHORIZED",
5 "message": "Authentication required"
6}res.forbidden(message?)
403'Insufficient permissions'1handler: (req, res) => {
2 if (!req.user.roles.includes('admin')) {
3 res.forbidden('Admin access required');
4 return;
5 }
6}1{
2 "success": false,
3 "error": "Forbidden",
4 "code": "FORBIDDEN",
5 "message": "Admin access required"
6}res.notFound(resource?)
404'Resource'1handler: (req, res) => {
2 const user = await getUser(req.params.id);
3 if (!user) {
4 res.notFound('User');
5 return;
6 }
7}1{
2 "success": false,
3 "error": "Not Found",
4 "code": "NOT_FOUND",
5 "message": "User not found"
6}res.conflict(message)
409required1handler: (req, res) => {
2 const existing = await getUserByEmail(req.body.email);
3 if (existing) {
4 res.conflict('Email already in use');
5 return;
6 }
7}1{
2 "success": false,
3 "error": "Conflict",
4 "code": "CONFLICT",
5 "message": "Email already in use"
6}res.validationError(errors)
422Sends field-level error details. Each error has field, message, and optional code.
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}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?)
429When retryAfter (seconds) is provided, the Retry-After header is set automatically.
1handler: (req, res) => {
2 const limited = await checkRateLimit(req.ip);
3 if (limited) {
4 res.rateLimited(60);
5 return;
6 }
7}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: 60res.internalError(message?)
500'Internal server error'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}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.
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.
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.
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.
1if (res.canSetHeaders()) {
2 res.setHeader('X-Custom', 'value');
3}res.getResponseState()
Returns the current ResponseState object.
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 { response } from '@morojs/moro';Quick Reference
| Function | Returns |
|---|---|
| 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", ... } |
1import { response } from '@morojs/moro';
2
3handler: (req, res) => {
4 const users = await getUsers();
5 return response.success(users);
6}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 { ResponseBuilder } from '@morojs/moro';Builder Methods
| Method | Description |
|---|---|
| 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 |
1handler: (req, res) => {
2 const users = await getUsers();
3 return ResponseBuilder.success(users)
4 .message('Retrieved all active users')
5 .build();
6}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.
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)
1handler: (req, res) => {
2 res.forbidden('You do not have access');
3 return;
4}Option 2: res.status().json() with standalone helper
1handler: (req, res) => {
2 return res.status(403).json(response.forbidden('You do not have access'));
3}Option 3: res.status().json() manually
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?)200res.created(data, loc?)201res.noContent()204res.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?)200res.badRequest(msg?)400res.unauthorized(msg?)401res.forbidden(msg?)403res.notFound(resource?)404res.conflict(msg)409res.validationError(errs)422res.rateLimited(retry?)429res.internalError(msg?)500