Skip to content

Migrating to 0.5

This guide covers changes introduced in ergo 0.5.0 and ergo-router 0.5.0. Both packages should be upgraded together — ergo-router 0.5.x requires @centralping/ergo >=0.5.0 <0.6.0.

Terminal window
npm install @centralping/ergo@^0.5.0 @centralping/ergo-router@^0.5.0

Verify versions after install:

Terminal window
npm ls @centralping/ergo @centralping/ergo-router

Potentially Breaking: Registration-Time Config Validation (ergo-router)

Section titled “Potentially Breaking: Registration-Time Config Validation (ergo-router)”

Routes that combine body: false with validate: {body: schema} now throw at registration time instead of producing a guaranteed 500 error on every request. The check uses resolved values — contradictions originating from defaults are also caught.

import createRouter from '@centralping/ergo-router';
const router = createRouter();
// ⚠️ This route silently produced a 500 on every request
router.post('/items', {
body: false,
validate: {body: itemSchema},
execute: createItem,
});
// Server starts without error — the bug surfaces at request time
import createRouter from '@centralping/ergo-router';
const router = createRouter();
// ✓ Now throws immediately at registration time:
// "Route config for POST /items has body: false but validate.body
// is configured. Body parsing must be enabled for body validation
// to work."
router.post('/items', {
body: false,
validate: {body: itemSchema},
execute: createItem,
});
  1. Start your application — if any routes have this contradiction, you will see an immediate error with the route method and path.
  2. Fix the contradiction — either remove body: false (to allow body parsing) or remove validate: {body: schema} (if body validation is not needed).
  3. Check defaults — the validation resolves config against defaults, so a defaults: {validate: {body: schema}} combined with a per-route body: false will also throw.

These are non-breaking additions in 0.5.0. No migration is required, but you may want to adopt them.

Pass a raw JSON Schema directly to validate() instead of wrapping it in {body: schema}:

router.post('/users', {
validate: {body: userSchema},
execute: createUser,
});

The shorthand is detected when the argument contains JSON Schema keywords (type, properties, required, etc.) and none of the targeted keys (body, query, params). The targeted form is unchanged and takes precedence when any targeted key is present.

See the validate middleware guide for the full detection algorithm and precedence rules.

responseSchema strips undeclared properties from response bodies before serialization — schema-based output projection:

router.get('/users/:id', {
send: {
responseSchema: {
200: {
type: 'object',
properties: {
id: {type: 'string'},
name: {type: 'string'},
},
},
},
},
execute: async (req, res, acc) => {
const user = await db.findById(acc.route.params.id);
// Even if `user` has extra fields (email, passwordHash),
// only `id` and `name` are sent to the client
return {response: {body: user}};
},
});

Resolution order: exact status code match → range key ('2xx') → 'default'. Only applies to Object bodies with statusCode < 400. Projectors are compiled at factory time for zero per-request overhead.

See the send middleware guide for the full resolution table and interaction with the response envelope.

onResponse Lifecycle Hook (ergo + ergo-router)

Section titled “onResponse Lifecycle Hook (ergo + ergo-router)”

Post-send observation hook for audit logging, metrics, and monitoring:

const router = createRouter({
onResponse: (req, res, responseInfo, domainAcc) => {
auditLog.write({
method: responseInfo.method,
url: responseInfo.url,
status: responseInfo.statusCode,
duration: responseInfo.duration,
user: domainAcc.auth?.identity,
});
},
});

Configurable at router level and per-route. Route-level hooks fire first, then router-level. Hook errors are silently swallowed — they cannot affect the response.

See the handler guide for standalone usage and the ergo-router package page for the dual-level execution model.

Custom error handlers now receive the domain accumulator as a fourth argument:

router.post('/orders', {
catchHandler: (req, res, err, domainAcc) => {
// domainAcc contains route params, parsed body, auth identity,
// and other pipeline data available at the time of the error
logger.error({
error: err.message,
user: domainAcc.auth?.identity,
route: domainAcc.route?.params,
});
res.writeHead(500, {'Content-Type': 'application/json'});
res.end(JSON.stringify({error: 'Internal Server Error'}));
},
authorization: {strategies: [bearerStrategy]},
validate: {body: orderSchema},
execute: processOrder,
});

Backwards compatible — existing 3-argument handlers continue to work.

Three new presets join presets.jsonApi:

  • presets.sse — Server-Sent Events: disables compression and timeout, restricts to text/event-stream
  • presets.webhooks — Webhook receiver: requires Idempotency-Key header, restricts to application/json
  • presets.public — Public read-only API: enables transport-level rate limiting (100 req/60s), sets Cache-Control: public, max-age=300
import createRouter, {presets} from '@centralping/ergo-router';
const router = createRouter({
...presets.sse,
defaults: {
...presets.sse.defaults,
authorization: {strategies: [bearerStrategy]},
},
});

See the ergo-router package page for per-preset inventory tables and override patterns.

These changes require no action but are worth noting.

The ergo test suite now runs on Deno 2.x and Bun 1.x alongside Node.js 22/24. Both runtimes pass all contract tests (100%) and nearly all unit tests via their Node.js compatibility layers. These are informational CI checks — Node.js remains the primary runtime.

@types/node Optional Peer Dependency (ergo + ergo-router)

Section titled “@types/node Optional Peer Dependency (ergo + ergo-router)”

Both packages now declare @types/node (>= 22) as an optional peer dependency. TypeScript consumers compiling with skipLibCheck: false need @types/node installed for .d.ts files that reference import('node:http'). JavaScript-only consumers are unaffected — npm prints an informational warning but does not fail.

If you are upgrading from a version earlier than 0.4.x, the following breaking changes also apply. See the 0.3-to-0.4 migration guide for the composition format change (the primary 0.4.0 breaking change) and its earlier cumulative entries.

From 0.1.x: Validate Body Prerequisite (ergo 0.2.0)

Section titled “From 0.1.x: Validate Body Prerequisite (ergo 0.2.0)”

validate({body: schema}) now returns a 500 response when body() middleware is not in the pipeline ahead of it. A one-time process.emitWarning diagnostic is emitted with code ERGO_VALIDATE_NO_BODY. Previously, body validation was silently skipped, allowing invalid request data to reach the execute handler.

Correctly ordered pipelines are unaffected — this only surfaces when validate() is placed before body() (or when body() is missing entirely).

From 0.1.x: Idempotency Malformed Header (ergo 0.2.0)

Section titled “From 0.1.x: Idempotency Malformed Header (ergo 0.2.0)”

idempotency() now distinguishes missing from malformed Idempotency-Key headers. A malformed header (present but not a valid RFC 8941 sf-string) always returns 400 with format guidance, regardless of the required option.

Previously, a malformed header with required: false silently passed through as if the header were absent.

After migrating, confirm everything works:

  1. Application starts — no registration-time errors from the body: false + validate.body semantic check.
  2. Build passesnpm run build (or your project’s build command) exits without errors.
  3. Tests passnpm test exits without failures.
  4. No runtime warnings — check for process.emitWarning diagnostics during startup (ergo validates option keys at factory time).
  5. Review changelog — see the full ergo changelog and ergo-router changelog for additional non-breaking additions you may want to adopt.