Getting Started
Prerequisites
Section titled “Prerequisites”- Node.js 22 or later (pure ESM)
- npm (ships with Node.js)
Installation
Section titled “Installation”npm install @centralping/ergo @centralping/ergo-routernpm install @centralping/ergoWhich Packages Do I Need?
Section titled “Which Packages Do I Need?”| Use case | Packages | Why |
|---|---|---|
| Full REST API with routing, transport security, and declarative pipeline assembly | @centralping/ergo + @centralping/ergo-router | ergo-router provides createRouter(), automatic handler()/send() wrapping, and transport-level concerns (request IDs, CORS, rate limiting, security headers) |
| Custom routing or framework integration — you bring your own router (Express, Fastify, Koa, etc.) | @centralping/ergo only | Use compose() and handler() directly to build pipelines without ergo-router’s opinions |
Quick Start
Section titled “Quick Start”Create a server with a single route that demonstrates all four Fast Fail stages:
import createRouter, {graceful} from '@centralping/ergo-router';
const router = createRouter({ defaults: { accepts: {types: ['application/json']}, },});
router.get('/users/:id', { authorization: { strategies: [ { type: 'Bearer', authorizer: async (attributes, token) => { if (!token) return {authorized: false}; return {authorized: true, info: {userId: token}}; }, }, ], }, validate: { params: { type: 'object', properties: {id: {type: 'string', minLength: 1}}, required: ['id'], }, }, execute: (req, res, acc) => ({ response: { body: { id: acc.route.params.id, name: 'Jane Doe', requestedBy: acc.auth.userId, }, }, }),});
router.post('/users', { authorization: { strategies: [ { type: 'Bearer', authorizer: async (attributes, token) => { if (!token) return {authorized: false}; return {authorized: true, info: {userId: token}}; }, }, ], }, validate: { body: { type: 'object', properties: {name: {type: 'string', minLength: 1}}, required: ['name'], }, }, execute: (req, res, acc) => ({ response: { statusCode: 201, body: {name: acc.body.parsed.name, createdBy: acc.auth.userId}, }, }),});
await graceful(router.handle(), {port: 3000});import createRouter, { defineGet, definePost, graceful,} from '@centralping/ergo-router';
const bearerStrategy = { type: 'Bearer' as const, authorizer: async (_: Record<string, string>, token: string) => { if (!token) return {authorized: false}; return {authorized: true, info: {userId: token}}; },};
const router = createRouter({ defaults: { accepts: {types: ['application/json']}, },});
router.get( '/users/:id', defineGet( { authorization: {strategies: [bearerStrategy]}, validate: { params: { type: 'object', properties: {id: {type: 'string', minLength: 1}}, required: ['id'], }, }, }, (req, res, acc) => ({ response: { body: { id: acc.route.params.id, name: 'Jane Doe', requestedBy: acc.auth.userId, }, }, }), ),);
router.post( '/users', definePost( { authorization: {strategies: [bearerStrategy]}, validate: { body: { type: 'object', properties: {name: {type: 'string', minLength: 1}}, required: ['name'], }, }, }, (req, res, acc) => ({ response: { statusCode: 201, body: {name: acc.body.parsed.name, createdBy: acc.auth.userId}, }, }), ),);
await graceful(router.handle(), {port: 3000});What Happens
Section titled “What Happens”| Request | Result |
|---|---|
GET /users/42 with Accept: text/xml | 406 Not Acceptable — negotiation fails |
GET /users/42 without Authorization | 401 Unauthorized — authorization fails |
GET /users/42 with valid Bearer token | 200 OK — all stages pass |
POST /users without Content-Type | 415 Unsupported Media Type — missing content type |
POST /users with invalid JSON body | 400 Bad Request — body parsing fails (malformed JSON) |
POST /users with valid JSON + Bearer token | 201 Created — resource created |
Every error response is a RFC 9457 Problem Details JSON object.
Using ergo Without ergo-router
Section titled “Using ergo Without ergo-router”If you chose the ergo-only path, you compose middleware manually using
compose() and wrap the pipeline with handler():
import http from 'node:http';import {compose, handler, logger, url, accepts} from '@centralping/ergo';
const pipeline = compose( logger(), accepts({types: ['application/json']}), url(), (req, res, acc) => ({ response: { body: {path: acc.url.pathname, query: acc.url.query}, statusCode: 200, }, }),);
const server = http.createServer(handler(pipeline));server.listen(3000, () => console.log('Listening on :3000'));Built-in middleware carries its own accumulator path — compose it as a
bare function call. Custom middleware uses {fn, setPath} config objects
to declare the key under which its result is stored on the
domain accumulator. The final function in the
pipeline is the execute step: it reads accumulated values and returns a
{response} object.
handler() creates both accumulators, runs the pipeline, and calls send()
exactly once to serialize the HTTP response. See the
handler guide for all available options.
Next Steps
Section titled “Next Steps”- TypeScript — tsconfig setup, type imports, and typed route helpers
- Fast Fail Pipeline — understand the four stages in depth
- Middleware — per-middleware guides with options, examples, and error responses
- Custom Middleware — write middleware that fits the accumulator contract
- Debugging & Diagnostics — startup errors, runtime warnings, warning codes, and troubleshooting FAQ
- Structured Logging — integrate pino or winston with ergo’s logging surfaces
- Testing Patterns — reliable HTTP testing with ephemeral ports, auth, validation, and graceful shutdown
- Production Deployment — full-stack deployment with Docker, nginx, health checks, and graceful shutdown
- Standards Compliance — full list of implemented RFCs
- Security — how ergo addresses the OWASP API Security Top 10
- ergo API — middleware overview and package reference
- ergo-router API — router and pipeline builder
- Changelog — release history and breaking change notices
- Migration Guides — step-by-step upgrade instructions with before/after examples