Skip to content

send

The terminal response writer in ergo’s two-accumulator model. Called once by handler() (or ergo-router’s auto-wrap) after the pipeline completes. Reads from both the response accumulator and domain accumulator (acc) to serialize the HTTP response.

send is not placed inside the pipeline — it runs after the pipeline via handler().

Pipeline stage: Post-pipeline (called by handler)

import {send} from '@centralping/ergo';
Option Type Default Description
prettify boolean false Pretty-print JSON output
vary string[] ['Accept'] Vary header values to append
etag boolean true Generate ETags and evaluate conditional headers
prefer boolean false Read domainAcc.prefer for RFC 7240 return=minimal / return=representation. When true, appends Prefer to the Vary header
paginate boolean false Read domainAcc.paginate and responseAcc.paginate to auto-generate RFC 8288 Link headers and X-Total-Count for paginated responses
envelope boolean | function false Wrap 2xx Object bodies in a response envelope
errorFormatter function Custom error body formatter for 4xx/5xx responses. Receives the RFC 9457 Problem Details object and {requestId, statusCode, method} context. Return value becomes the response body as application/json instead of application/problem+json
responseSchema Record<number|string, object> Map of status code (or range key like '2xx', '2XX', 'default') to JSON Schema object. When provided, response bodies for matching success status codes are projected through a compiled schema projector that strips undeclared properties before serialization
Value Behavior
false No envelope (default)
true Built-in {id, status, data, count?}id is read from the x-request-id response header
function Custom (body, ctx) => wrappedBody
Lookup Match
Exact status code (200) Uses the schema keyed to that code
Range ('2xx' or '2XX') Falls back to the range key for the status class
'default' Falls back to the 'default' key
No match No projection — body passes through unchanged

None. send() writes directly to the HTTP response and is called by handler() after the pipeline completes.

Error bodies are automatically formatted as RFC 9457 Problem Details from the response accumulator’s statusCode, detail, retryAfter, instance, and any extension members.

import http from 'node:http';
import {handler, compose} from '@centralping/ergo';
const pipeline = compose(
async (req, res, acc) => {
const user = await db.findUser('42');
return {
response: {statusCode: 200, body: user},
};
},
);
const server = http.createServer(
handler(pipeline, {
responseSchema: {
200: {
type: 'object',
properties: {
id: {type: 'number'},
name: {type: 'string'},
email: {type: 'string'},
},
},
},
}),
);
// GET /users/42
// Body { id, name, email } — internal fields (passwordHash, etc.) stripped

Response accumulator: statusCode, body, headers, detail, retryAfter, instance, location, lastModified, type, paginate (pagination response metadata: total, nextCursor, prevCursor)

Domain accumulator: cookies (cookie jar → Set-Cookie), prefer (parsed Prefer header), paginate (parsed pagination parameters)

For success responses (statusCode < 400). Error responses use RFC 9457 Problem Details formatting instead.

Body Type Content-Type Behavior
null / undefined Default status text (empty for 204/304)
string Auto-detected HTML or text Written as-is
Uint8Array application/octet-stream Written as binary
Stream Piped to response
Object application/json JSON-serialized (respects prettify)
Status Condition
304 Not Modified If-None-Match weak-matches the ETag, or If-Modified-Since and resource not modified
412 Precondition Failed If-Match fails on unsafe methods, or If-Unmodified-Since fails
204 No Content Prefer: return=minimal on 2xx responses (200 → 204)

Return lastModified on the response accumulator to set the Last-Modified header and enable date-based conditional request evaluation. The value can be a Date or a date string — unparseable values are silently ignored (no header set, no conditional evaluation).

import http from 'node:http';
import {handler, compose} from '@centralping/ergo';
const pipeline = compose(
async (req, res, acc) => {
const todo = await db.findById('42');
return {
response: {
statusCode: 200,
body: todo,
lastModified: todo.updatedAt,
},
};
},
);
const server = http.createServer(handler(pipeline));
// GET /todos/42
// → 200 OK
// → Last-Modified: Thu, 05 Jun 2025 12:00:00 GMT

When a client sends the Last-Modified value back in an If-Modified-Since header, send() automatically returns 304 Not Modified with no body if the resource has not changed. Dates are compared at second granularity.

GET /todos/42
If-Modified-Since: Thu, 05 Jun 2025 12:00:00 GMT
→ 304 Not Modified (no body)

For unsafe methods (PUT, PATCH, DELETE), send() evaluates If-Unmodified-Since as a write-protection guard. If the resource was modified after the condition date, the request is rejected with 412 Precondition Failed.

import http from 'node:http';
import {handler, compose, body} from '@centralping/ergo';
const pipeline = compose(
body(),
async (req, res, acc) => {
const todo = await db.findAndUpdate('42', acc.body.parsed);
return {
response: {
statusCode: 200,
body: todo,
lastModified: todo.updatedAt,
},
};
},
);
const server = http.createServer(handler(pipeline));
// PUT /todos/42
// If-Unmodified-Since: Thu, 05 Jun 2025 11:00:00 GMT
// (resource modified at 12:00:00 — after the condition date)
// → 412 Precondition Failed
import http from 'node:http';
import {handler, compose, send} from '@centralping/ergo';
const pipeline = compose(
(req, res, acc) => ({
response: {statusCode: 200, body: {hello: 'world'}},
}),
);
const server = http.createServer(
handler(pipeline, {etag: true, prettify: true}),
);
  • Pagination — End-to-end offset and cursor pagination with auto-generated Link headers

See the auto-generated send API docs.