{"_id":"ctx-router","_rev":"34-68fcb869086b102652b182365e8e65ee","name":"ctx-router","dist-tags":{"latest":"0.5.0"},"versions":{"0.3.0":{"name":"ctx-router","version":"0.3.0","keywords":["ctx-router","context-router","framework-agnostic","transport-agnostic","portable-api","express","fastify","koa","lambda","aws-lambda","sqs","kinesis","grpc","serverless","microservices","clean-architecture","hexagonal-architecture","multi-transport","api-router","event-driven","cloud-agnostic","azure-functions","google-cloud-functions"],"author":{"name":"Kaushik R Bangera"},"license":"MIT","_id":"ctx-router@0.3.0","maintainers":[{"name":"kaushikrb","email":"kaushikrb909@gmail.com"}],"homepage":"https://github.com/NeuronEnix/ctx-router#readme","bugs":{"url":"https://github.com/NeuronEnix/ctx-router/issues"},"dist":{"shasum":"0bfc6fff46efe1b069ec9df1b9cc793dc3ad2edc","tarball":"https://registry.npmjs.org/ctx-router/-/ctx-router-0.3.0.tgz","fileCount":71,"integrity":"sha512-sqCgffJLSxuIb/6DraP/O1WatHLas0DQA7JB2P3KeuvXY6gb344EE6xReJSOadvOfw0rdqNemw+ynd/urdg52A==","signatures":[{"sig":"MEQCIBOSH9102VjTQh9Nc2U44RKdWGjxwTAG4nV8qAg7m/HZAiBDVZyl3rD8xoi+cNuALEP8eQpHpV/YfKH5tbNGlaIzrg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/ctx-router@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":129930},"main":"dist/index.js","pnpm":{"overrides":{"qs":">=6.14.1","diff":">=8.0.3","body-parser":">=2.2.1"}},"types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"gitHead":"e62f5320bcde76db6a0eae024d4d915567268243","scripts":{"dev":"pnpm --filter ctx-router-example-express dev","lint":"eslint --fix src","test":"vitest run","build":"tsc","format":"prettier --write src","prepack":"pnpm build:clean","prepare":"husky","test:watch":"vitest","build:clean":"node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc","lint-staged":"lint-staged --allow-empty","lint:staged":"lint-staged --config .lintstagedrc.json --allow-empty","format:staged":"lint-staged --config .lintstagedrc.json --allow-empty","prepublishOnly":"pnpm build:clean && pnpm test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:81aa88ea-a669-4435-bc70-924db2747572"}},"repository":{"url":"git+https://github.com/NeuronEnix/ctx-router.git","type":"git"},"_npmVersion":"11.6.2","description":"Framework-agnostic router for building portable APIs. Write your business logic once, run it on Express, Lambda, SQS, gRPC, or any transport layer.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","dependencies":{"path-to-regexp":"^8.3.0"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","eslint":"^9.33.0","vitest":"^4.0.16","express":"^5.2.1","nodemon":"^3.1.11","ts-node":"^10.9.2","prettier":"^3.6.2","@eslint/js":"^9.33.0","typescript":"^5.9.3","@types/node":"^24.10.4","lint-staged":"^16.1.5","@types/express":"^5.0.3","@eslint/eslintrc":"^3.3.3","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.4","@typescript-eslint/parser":"^8.39.1","@typescript-eslint/eslint-plugin":"^8.39.1"},"_npmOperationalInternal":{"tmp":"tmp/ctx-router_0.3.0_1771179569618_0.6081577291456746","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"ctx-router","version":"0.4.0","keywords":["ctx-router","context-router","framework-agnostic","transport-agnostic","portable-api","express","fastify","koa","lambda","aws-lambda","sqs","kinesis","grpc","serverless","microservices","clean-architecture","hexagonal-architecture","multi-transport","api-router","event-driven","cloud-agnostic","azure-functions","google-cloud-functions"],"author":{"name":"Kaushik R Bangera"},"license":"MIT","_id":"ctx-router@0.4.0","maintainers":[{"name":"kaushikrb","email":"kaushikrb909@gmail.com"}],"homepage":"https://github.com/NeuronEnix/ctx-router#readme","bugs":{"url":"https://github.com/NeuronEnix/ctx-router/issues"},"dist":{"shasum":"92bd7810e09dfd564ba32bb3cf2b77ecf1f0731e","tarball":"https://registry.npmjs.org/ctx-router/-/ctx-router-0.4.0.tgz","fileCount":71,"integrity":"sha512-rgU02qpsKnZ0J/NIM963T6WgMh/n356hMRLhySjXLz8Tx1s3e5GEdXDYLVvA0vp7rrnydrJbRsBap4kkQm7NuA==","signatures":[{"sig":"MEUCIB++gtYIPI0VX4Ri8sTGIHNfyCAyEXCFWqm7w9Ne8/8DAiEA0vEzGh1yoaBPBPn0CDFLCiPi1tZ9a8mwSMUbePf1Bz0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/ctx-router@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":129539},"main":"dist/index.js","pnpm":{"overrides":{"qs":">=6.14.1","diff":">=8.0.3","body-parser":">=2.2.1"}},"types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"gitHead":"74e9b444baa45a8cf350ab6fd68941b8f953b703","scripts":{"dev":"pnpm --filter ctx-router-example-express dev","lint":"eslint --fix src","test":"vitest run","build":"tsc","format":"prettier --write src","prepack":"pnpm build:clean","prepare":"husky","test:watch":"vitest","build:clean":"node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc","lint-staged":"lint-staged --allow-empty","lint:staged":"lint-staged --config .lintstagedrc.json --allow-empty","format:staged":"lint-staged --config .lintstagedrc.json --allow-empty","prepublishOnly":"pnpm build:clean && pnpm test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:81aa88ea-a669-4435-bc70-924db2747572"}},"repository":{"url":"git+https://github.com/NeuronEnix/ctx-router.git","type":"git"},"_npmVersion":"11.9.0","description":"Framework-agnostic router for building portable APIs. Write your business logic once, run it on Express, Lambda, SQS, gRPC, or any transport layer.","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","dependencies":{"path-to-regexp":"^8.3.0"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","eslint":"^9.33.0","vitest":"^4.0.16","express":"^5.2.1","nodemon":"^3.1.11","ts-node":"^10.9.2","prettier":"^3.6.2","@eslint/js":"^9.33.0","typescript":"^5.9.3","@types/node":"^24.10.4","lint-staged":"^16.1.5","@types/express":"^5.0.3","@eslint/eslintrc":"^3.3.3","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.4","@typescript-eslint/parser":"^8.39.1","@typescript-eslint/eslint-plugin":"^8.39.1"},"_npmOperationalInternal":{"tmp":"tmp/ctx-router_0.4.0_1773572144192_0.5883778612051074","host":"s3://npm-registry-packages-npm-production"}},"0.4.2":{"name":"ctx-router","version":"0.4.2","keywords":["ctx-router","context-router","framework-agnostic","transport-agnostic","portable-api","express","fastify","koa","lambda","aws-lambda","sqs","kinesis","grpc","serverless","microservices","clean-architecture","hexagonal-architecture","multi-transport","api-router","event-driven","cloud-agnostic","azure-functions","google-cloud-functions"],"author":{"name":"Kaushik R Bangera"},"license":"MIT","_id":"ctx-router@0.4.2","maintainers":[{"name":"kaushikrb","email":"kaushikrb909@gmail.com"}],"homepage":"https://github.com/NeuronEnix/ctx-router#readme","bugs":{"url":"https://github.com/NeuronEnix/ctx-router/issues"},"dist":{"shasum":"68b15fa1a4bf053db46645241b71153d62fff602","tarball":"https://registry.npmjs.org/ctx-router/-/ctx-router-0.4.2.tgz","fileCount":71,"integrity":"sha512-7Lfy3fAZaengsq3oz4Xu1qqT8JrhnacbyOnNkUB06xH5gF51+M0qA+4I1b8IhE/VFkBrI1maowgT/G4ZYgrPLw==","signatures":[{"sig":"MEUCIQDvzfUo/FKwFWdoPOBDCTX6u7DVpmPOI+12+/RVB4tt1wIgTiedhl/cVNMLaCKdofLrP/v358SgHT1KGzJN4zc8p0M=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/ctx-router@0.4.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":129240},"main":"dist/index.js","pnpm":{"overrides":{"qs":">=6.14.1","diff":">=8.0.3","vite":">=7.3.2","body-parser":">=2.2.1"}},"types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"gitHead":"315f794fa1b726bc69109c4fec47dd4c9ba2f35d","scripts":{"dev":"pnpm --filter ctx-router-example-express dev","lint":"eslint --fix src","test":"vitest run","build":"tsc","format":"prettier --write src","prepack":"pnpm build:clean","prepare":"husky","test:watch":"vitest","build:clean":"node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc","lint-staged":"lint-staged --allow-empty","lint:staged":"lint-staged --config .lintstagedrc.json --allow-empty","format:staged":"lint-staged --config .lintstagedrc.json --allow-empty","prepublishOnly":"pnpm build:clean && pnpm test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:81aa88ea-a669-4435-bc70-924db2747572"}},"repository":{"url":"git+https://github.com/NeuronEnix/ctx-router.git","type":"git"},"_npmVersion":"11.12.1","description":"Framework-agnostic router for building portable APIs. Write your business logic once, run it on Express, Lambda, SQS, gRPC, or any transport layer.","directories":{},"sideEffects":false,"_nodeVersion":"24.15.0","dependencies":{"path-to-regexp":"^8.4.2"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.26.2","devDependencies":{"vite":"^7.3.3","husky":"^9.1.7","eslint":"^10.4.0","vitest":"^4.1.6","express":"^5.2.1","nodemon":"^3.1.14","ts-node":"^10.9.2","prettier":"^3.8.3","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^25.8.0","lint-staged":"^17.0.5","@types/express":"^5.0.6","@eslint/eslintrc":"^3.3.5","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.5","@typescript-eslint/parser":"^8.59.3","@typescript-eslint/eslint-plugin":"^8.59.3"},"_npmOperationalInternal":{"tmp":"tmp/ctx-router_0.4.2_1778944759155_0.9922825380958642","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"ctx-router","version":"0.5.0","description":"Framework-agnostic router for building portable APIs. Write your business logic once, run it on Express, Lambda, SQS, gRPC, or any transport layer.","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"sideEffects":false,"scripts":{"build":"tsc","build:clean":"node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc","prepublishOnly":"pnpm build:clean && pnpm test","prepack":"pnpm build:clean","dev":"pnpm --filter ctx-router-example-express dev","format":"prettier --write src","format:staged":"lint-staged --config .lintstagedrc.json --allow-empty","lint":"eslint --fix src","lint:staged":"lint-staged --config .lintstagedrc.json --allow-empty","lint-staged":"lint-staged --allow-empty","prepare":"husky","test":"vitest run","test:watch":"vitest"},"repository":{"type":"git","url":"git+https://github.com/NeuronEnix/ctx-router.git"},"keywords":["ctx-router","context-router","framework-agnostic","transport-agnostic","portable-api","express","fastify","koa","lambda","aws-lambda","sqs","kinesis","grpc","serverless","microservices","clean-architecture","hexagonal-architecture","multi-transport","api-router","event-driven","cloud-agnostic","azure-functions","google-cloud-functions"],"author":{"name":"Kaushik R Bangera"},"license":"MIT","bugs":{"url":"https://github.com/NeuronEnix/ctx-router/issues"},"homepage":"https://github.com/NeuronEnix/ctx-router#readme","packageManager":"pnpm@10.26.2","devDependencies":{"@eslint/eslintrc":"^3.3.5","@eslint/js":"^10.0.1","@types/express":"^5.0.6","@types/node":"^25.8.0","@typescript-eslint/eslint-plugin":"^8.59.3","@typescript-eslint/parser":"^8.59.3","eslint":"^10.4.0","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.5","express":"^5.2.1","husky":"^9.1.7","lint-staged":"^17.0.5","nodemon":"^3.1.14","prettier":"^3.8.3","ts-node":"^10.9.2","typescript":"^6.0.3","vite":"^7.3.3","vitest":"^4.1.6"},"dependencies":{"path-to-regexp":"^8.4.2"},"pnpm":{"overrides":{"qs":">=6.14.1","body-parser":">=2.2.1","diff":">=8.0.3","vite":">=7.3.2"}},"gitHead":"c00c3d11e5aa04b8f874629d0ac10c203b345a7c","_id":"ctx-router@0.5.0","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-LIao2O5hxxJ6IAi6X3zZQFkr8NSj2QMHws1wBujQa5IGafXaCquPigAu1IZghSwNQbHmZ1UptGHVFF4CSZ9umQ==","shasum":"c046ade129cce0e0214e939124b05d95d7f7391e","tarball":"https://registry.npmjs.org/ctx-router/-/ctx-router-0.5.0.tgz","fileCount":71,"unpackedSize":116868,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/ctx-router@0.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIE8vdRUJWHBlwJN4jPxRWzn5jolYTMD7EZd2VOh8AgJhAiBWnrY3UKtCeKEE6pfRThxCxdkDNiuO15O1Ghyrdfip7Q=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:81aa88ea-a669-4435-bc70-924db2747572"}},"directories":{},"maintainers":[{"name":"kaushikrb","email":"kaushikrb909@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ctx-router_0.5.0_1778995084734_0.36783471550223945"},"_hasShrinkwrap":false}},"time":{"created":"2025-08-17T13:59:38.682Z","modified":"2026-05-17T05:18:05.135Z","1.0.0":"2025-08-17T13:59:38.857Z","1.0.1":"2025-08-17T15:05:07.947Z","1.0.2":"2025-08-17T15:56:40.581Z","1.0.3":"2025-08-17T16:13:12.717Z","1.0.4":"2025-12-01T04:29:55.531Z","1.0.5":"2025-12-01T04:37:31.364Z","1.0.6":"2025-12-01T09:54:16.311Z","1.0.7":"2025-12-01T10:02:27.271Z","1.0.8":"2025-12-30T01:26:24.600Z","0.1.0":"2026-01-18T09:05:08.510Z","0.2.0":"2026-02-15T07:30:22.723Z","0.2.1":"2026-02-15T08:32:54.571Z","0.2.6":"2026-02-15T16:41:41.920Z","0.2.7":"2026-02-15T16:49:19.670Z","0.3.0":"2026-02-15T18:19:29.779Z","0.4.0":"2026-03-15T10:55:44.356Z","0.4.2":"2026-05-16T15:19:19.315Z","0.5.0":"2026-05-17T05:18:04.883Z"},"bugs":{"url":"https://github.com/NeuronEnix/ctx-router/issues"},"author":{"name":"Kaushik R Bangera"},"license":"MIT","homepage":"https://github.com/NeuronEnix/ctx-router#readme","keywords":["ctx-router","context-router","framework-agnostic","transport-agnostic","portable-api","express","fastify","koa","lambda","aws-lambda","sqs","kinesis","grpc","serverless","microservices","clean-architecture","hexagonal-architecture","multi-transport","api-router","event-driven","cloud-agnostic","azure-functions","google-cloud-functions"],"repository":{"type":"git","url":"git+https://github.com/NeuronEnix/ctx-router.git"},"description":"Framework-agnostic router for building portable APIs. Write your business logic once, run it on Express, Lambda, SQS, gRPC, or any transport layer.","maintainers":[{"name":"kaushikrb","email":"kaushikrb909@gmail.com"}],"readme":"# ctx-router\n\n[![npm version](https://img.shields.io/npm/v/ctx-router.svg)](https://www.npmjs.com/package/ctx-router)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nA transport-agnostic router that normalizes every ingress — HTTP, Lambda, SQS, gRPC, anything — into a single `ctx` object and runs your business logic as a linear pipeline.\n\nWrite your handlers once. Run them on Express today, Lambda tomorrow, without touching a line of business code.\n\n## Why ctx-router?\n\nApps usually start with HTTP, then grow background jobs, async workers, serverless triggers. The same business logic ends up duplicated across each transport, glued together with framework-specific objects.\n\n**The problem isn't routing — it's transport leakage into business logic.**\n\n`ctx-router` normalizes all ingress into one `ctx`, routes by pattern (not framework), and runs middleware + handler as a linear pipeline. Transport concerns stay at the boundaries.\n\n## Installation\n\n```bash\nnpm install ctx-router\n```\n\n## Quick Start\n\n### 1. Define your router\n\n```typescript\n// router.ts\nimport { CtxRouter, CtxType, CtxErr } from \"ctx-router\";\n\nexport type TCtx = CtxType.DefaultCtx & {\n  user: { role: (\"user\" | \"admin\")[] };\n};\n\nclass AppErr extends CtxErr.BaseError {\n  constructor(e: CtxType.BaseError) {\n    super(e);\n  }\n}\n\nexport const appErr = CtxErr.errMap(AppErr, {\n  auth: { UNAUTHORIZED: \"Unauthorized\" },\n  general: { UNKNOWN_ERROR: \"Something went wrong\" },\n});\n\nexport const router = new CtxRouter<TCtx>({ serviceName: \"my-service\" });\n```\n\n### 2. Register routes\n\n```typescript\n// HTTP grammar is auto-detected\nrouter.route(\"GET /health\").to(api.health.ping);\n\n// Middleware chain\nrouter.route(\"POST /user/update\").via(auth, validate).to(api.user.update);\n\n// Scoped builder with shared middleware\nconst userScope = router.route(\"/user\").via(authMiddleware);\nuserScope.route(\"GET /:userId\").to(api.user.detail);\n```\n\n### 3. Write a handler\n\n```typescript\n// api/user/userUpdate.ts\nexport async function update(ctx: TCtx): Promise<TCtx> {\n  const { userId, name } = ctx.req.data;\n  // Business logic — no Express, no Lambda, just ctx\n  ctx.res.data = { userId, name };\n  return ctx;\n}\n```\n\n### 4. Wire up Express\n\n```typescript\n// server.ts\nimport express from \"express\";\nimport { CtxAdapter } from \"ctx-router\";\nimport { router } from \"./router\";\n\nconst app = express();\napp.use(express.json());\n\napp.use(async (req, res) => {\n  const ctx = router.newCtx();\n  CtxAdapter.enrichFromExpress(ctx, req, res);\n  await router.exec(ctx);\n  res.status(ctx.res.code === \"OK\" ? 200 : 400).json(ctx.res);\n});\n\napp.listen(3000);\n```\n\n## The Context\n\n`CtxType.DefaultCtx` is the single object that flows through your system:\n\n```typescript\ntype DefaultCtx = {\n  id: string; // traceId, set during exec()\n  req: CtxReq; // route, data, auth, caller, transport\n  res: CtxRes; // { code, msg, data }\n  user: CtxUser; // discriminated union (user | service)\n  meta: CtxMeta; // service, instance, timing, monitor\n  locals: Record<string, unknown>;\n  err: CtxErr.BaseError | null;\n};\n```\n\n`ctx.req.data` is the unified input payload — the adapter merges path params, query, and body into it. On key collisions **body wins**, then query, then path params.\n\nExtend `DefaultCtx` for app-specific fields:\n\n```typescript\nexport type TCtx = CtxType.DefaultCtx & {\n  user: { role: (\"user\" | \"admin\")[] };\n  locals: { db?: DatabaseConnection };\n};\n```\n\nFull type definitions live in [`src/core/`](./src/core).\n\n## Route builder\n\nRoutes are built via an immutable fluent DSL: `route()` → `via()` → `to()`.\n\n```typescript\nrouter.route(segment).via(mw1, mw2).to(handler);\n```\n\n**HTTP grammar auto-detection.** Whitespace-split each segment; tokens matching `GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS` become the route's `op`, the rest is the pattern.\n\n```typescript\nrouter.route(\"GET /user/:id\"); // op: \"GET\", pattern: \"/user/:id\"\n```\n\n**Scoped builders.** Each `.route()`/`.via()` returns a new builder — reuse parents to share middleware.\n\n```typescript\nconst userScope = router.route(\"/user\").via(authMw);\nuserScope.route(\"GET /:id\").to(api.user.detail);\nuserScope.route(\"POST /update\").to(api.user.update);\n```\n\n**Global middleware.** `router.via(...)` returns a restricted scope (`route`/`via` only — no `.to()`) so middleware can be applied before any route definition.\n\n```typescript\nrouter.via(logMw, metricsMw).route(\"GET /health\").to(api.health.ping);\n```\n\n**Path parameters.** Patterns with `:param` are matched via `path-to-regexp`; extracted params are merged into `ctx.req.data` with the lowest priority (anything in body/query wins).\n\n```typescript\nrouter.route(\"GET /user/:userId/post/:postId\").to(async (ctx) => {\n  // ctx.req.data.userId, ctx.req.data.postId\n  return ctx;\n});\n```\n\n## Lifecycle hooks\n\n```typescript\nrouter.hook.onExec.before(async (ctx) => {\n  /* setup, tracing */\n});\nrouter.hook.onExec.after(async (ctx) => {\n  /* metrics on success */\n});\nrouter.hook.onExec.error(async (ctx, err) => {\n  /* shape the error response */\n});\nrouter.hook.onExec.finally(async (ctx) => {\n  /* always runs */\n});\n```\n\nExecution order: `before` → route match → middleware → handler → `after` (or `error`) → `finally`.\n\nHooks are **sealed on the first `exec()` call** — register them during startup.\n\nIf `onExec.error` is registered, exec swallows the error and returns ctx (your hook writes the response). Without it, exec re-throws (fail-fast).\n\n## Error handling\n\nDefine an app error class and a structured error map:\n\n```typescript\nclass AppErr extends CtxErr.BaseError {\n  constructor(e: CtxType.BaseError) {\n    super(e);\n  }\n}\n\nexport const appErr = CtxErr.errMap(AppErr, {\n  auth: { UNAUTHORIZED: \"Unauthorized\", TOKEN_EXPIRED: \"Token expired\" },\n  user: { NOT_FOUND: \"User not found\" },\n});\n\n// In a handler\nthrow appErr.auth.UNAUTHORIZED();\nthrow appErr.user.NOT_FOUND({\n  data: { userId }, // client-safe\n  info: { stack }, // server-only\n});\n```\n\n`CtxErr.BaseError` instances expose `{ name, msg, data, info }`. `data` is intended for `ctx.res.data`; `info` stays server-side.\n\nShape the response in the error hook:\n\n```typescript\nrouter.hook.onExec.error(async (ctx, err) => {\n  if (err instanceof AppErr) {\n    ctx.res.code = err.name;\n    ctx.res.msg = err.message;\n    ctx.res.data = err.data;\n  } else {\n    ctx.res.code = \"UNKNOWN_ERROR\";\n    ctx.res.msg = \"An unexpected error occurred\";\n  }\n  ctx.err = err;\n});\n```\n\n## Express adapter\n\n```typescript\nimport { CtxAdapter } from \"ctx-router\";\nCtxAdapter.enrichFromExpress(ctx, req, res);\n```\n\nThe adapter populates `ctx.req` from the Express request:\n\n- **`data`** — merged `params + query + body` (body wins on collisions)\n- **`route`** — `op: req.method`, `raw: req.path`\n- **`auth`** — `Authorization: Bearer …` (→ `bearerToken`) or `Authorization: Basic …` (→ `clientId`/`clientSecret`); API key from `x-ctx-api-key` / `x-api-key` / `apikey` (first match); `x-ctx-refresh-token`\n- **`caller`** — identity (`x-ctx-app-version`, `x-ctx-api-version`, `x-ctx-session-id`, `x-ctx-device-id`) plus correlation hints (`x-ctx-trace-id`, `x-ctx-seq`, `x-ctx-client-ts`, `x-ctx-ingress-in`)\n- **`transport`** — `protocol: \"http\"`, `framework: \"express\"`, native `req`/`res` stashed in `raw`\n\n## Adding a new transport\n\nAdapters take whatever the platform hands you and mutate `ctx` in place. At minimum:\n\n- Set `ctx.req.data` (merged input, body-wins priority).\n- Set `ctx.req.route.op` and `ctx.req.route.raw`. Leave `pattern` as `\"PENDING\"` — the router rewrites it after matching.\n- Set `ctx.req.transport.protocol` and stash the native object(s) in `transport.raw`.\n\nSee [`src/adapter/express.v5.ts`](./src/adapter/express.v5.ts) as a reference.\n\n## API surface\n\n| Export                         | Notes                                                  |\n| ------------------------------ | ------------------------------------------------------ |\n| `CtxRouter`                    | Generic over `TUserCtx extends TDefaultCtx`            |\n| `DEFAULT_USER_ROLE`            | `{ none, user, admin, service }`                       |\n| `CtxType.*`                    | `DefaultCtx`, `CtxConsumerFn<T>`, `RouteBuilder<T>`, … |\n| `CtxErr.BaseError`             | Extend this for your app errors                        |\n| `CtxErr.errMap`                | Build a typed, category-organized error factory        |\n| `CtxAdapter.enrichFromExpress` | `(ctx, req, res) => void`, mutates in place            |\n\n`CtxRouter` methods: `newCtx()`, `exec(ctx)`, `route(...segments)`, `via(...mws)`, plus the `hook` DSL.\n\n## Contributing\n\nOpen an issue or pull request on [GitHub](https://github.com/NeuronEnix/ctx-router).\n\n## License\n\nMIT © Kaushik R Bangera\n","readmeFilename":"README.md"}