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
Section titled “Import”import {send} from '@centralping/ergo';Options
Section titled “Options”| 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 |
Envelope Values
Section titled “Envelope Values”| 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 |
Response Schema Resolution
Section titled “Response Schema Resolution”| 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 |
Return Value
Section titled “Return Value”None. send() writes directly to the HTTP response and is called by
handler() after the pipeline completes.
Error Responses
Section titled “Error Responses”Error bodies are automatically formatted as
RFC 9457 Problem Details from
the response accumulator’s statusCode, detail, retryAfter,
instance, and any extension members.
Response Schema
Section titled “Response Schema”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.) strippedconst router = createRouter({ defaults: { send: { responseSchema: { '2xx': { type: 'object', properties: { id: {type: 'number'}, name: {type: 'string'}, email: {type: 'string'}, }, }, }, }, },});Accumulator Inputs
Section titled “Accumulator Inputs”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)
Body Handling
Section titled “Body Handling”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) |
Conditional Requests
Section titled “Conditional Requests”| 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) |
Setting lastModified
Section titled “Setting lastModified”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 GMTrouter.get('/todos/:id', { execute: async (req, res, acc) => { const todo = await db.findById(acc.route.params.id); return { response: { statusCode: 200, body: todo, lastModified: todo.updatedAt, }, }; },});// GET /todos/42// → 200 OK// → Last-Modified: Thu, 05 Jun 2025 12:00:00 GMTWhen 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/42If-Modified-Since: Thu, 05 Jun 2025 12:00:00 GMT
→ 304 Not Modified (no body)Write Protection with If-Unmodified-Since
Section titled “Write Protection with If-Unmodified-Since”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 Failedrouter.put('/todos/:id', { execute: async (req, res, acc) => { const todo = await db.findAndUpdate( acc.route.params.id, acc.body.parsed, ); return { response: { statusCode: 200, body: todo, lastModified: todo.updatedAt, }, }; },});// 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 FailedBasic Usage
Section titled “Basic Usage”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}),);const router = createRouter({ defaults: { send: {etag: true, prefer: true}, },});RFC References
Section titled “RFC References”- RFC 9110 — HTTP Semantics
- RFC 9457 — Problem Details for HTTP APIs
- RFC 7240 — Prefer Header for HTTP
Related Recipes
Section titled “Related Recipes”- Pagination — End-to-end offset and cursor pagination with auto-generated Link headers
API Reference
Section titled “API Reference”See the auto-generated send API docs.