Skip to content

feat!: restana 7.0 – trie router, consistent errors, typed params, 100% coverage - #128

Merged
jkyberneees merged 7 commits into
masterfrom
fix/send-status-and-ergonomics
Oct 24, 2026
Merged

jkyberneees merged 7 commits into
masterfrom
fix/send-status-and-ergonomics

Conversation

@jkyberneees

Copy link
Copy Markdown
Collaborator

Summary

Major release (7.0.0). It contains breaking changes, and MIGRATION.md explains how to upgrade from 6.x. In short:

  • Router: restana now runs on 0http 5.2 and uses its dependency-free trie router by default.
  • Errors: the default error handler now produces the same responses as res.send(err).
  • Bug fixes: several bugs that silently produced wrong responses are fixed.
  • TypeScript: req.params is typed from the route path.
  • Startup: loading restana is about 40% faster.
  • Tests: coverage is now 100% and is enforced at a 99% minimum.

Breaking changes

Change How to keep the 6.x behavior
Trie router is the default. Paths are case-sensitive and invalid route patterns throw when registered. caseSensitive: false, or router: 'sequential'
routerCacheSize only applies to the sequential router. router: 'sequential'
The default errorHandler responds like res.send(err): a 404 says Not Found instead of Internal Server Error, and debugErrors now applies to it. Values that aren't Error objects are wrapped so they can't leak. Custom errorHandler (snippet in MIGRATION.md)
res.send(number): only whole numbers from 100 to 999 are status codes. Other numbers are sent as JSON instead of being dropped. –
res.send() with a function, symbol or bigint responds with a 500. A function used to be treated as the res.end() callback, which sent an empty 200. –
The exports map limits imports to restana, restana/libs/elastic-apm, restana/libs/newrelic-apm and restana/package.json. –
TypeScript: get/post/…/use() return the object they were called on. getConfigOptions() returns a read-only object and errorHandler is read-only. use(router) without a prefix is rejected because it already threw at runtime. –

Fixes

  • res.send(undefined, 201) and res.send(Promise.resolve(), 204) keep the status code that was passed in. Before, it was replaced with 200.
  • A text error code such as 'ENOENT' no longer hides a valid err.statusCode. Before, those errors came back as 500 instead of 404.
  • restana.Protocol and restana.Method now exist at runtime. The type definitions declared them, but they were undefined.
  • APM integrations name routes registered with several paths one path at a time, instead of joining them as GET /a,/b.

Added

  • Options: router ('trie' by default, or 'sequential'), plus caseSensitive and ignoreTrailingSlash.
  • TypeScript:
    • req.params is typed from the route path: '/users/:id' gives req.params.id. A new restana.RouteParams<Path> type covers handlers declared separately. Other keys are still typed as string, so existing code compiles unchanged.
    • use() accepts several middlewares in one call.
    • The APM modules now come with type definitions.
  • .nvmrc set to Node 26, the latest stable release. The minimum stays Node 24 LTS.

Performance

I profiled first. Under load, restana's own code is only about 2–3% of CPU, and Node's I/O takes most of the rest, so these changes target what the profile showed:

  • Startup: the sequential router and its packages load only when router: 'sequential' is chosen. require('restana') drops from about 16 ms to 9 ms, the heap starts about 0.5 MB smaller, and a default install loads no third-party packages.
  • res.send(): it no longer adds a finish listener to every response when no callback is given.
  • Security headers: their options are resolved once per service instead of on every request.

Benchmarks: the server was pinned to 1 core and autocannon to the other 3, with 100 connections. Numbers are medians in requests per second; run-to-run noise is about ±8%.

Runtime Startup 6.1 → 7.0 /hi 6.1 → 7.0 (alternating rounds)
Node 22.22 12.3 → 7.5 ms ~42.5k → ~45.0k (fixed-order matrix)
Node 24.22 LTS 11.9 → 8.9 ms ~54.0k → ~57.6k (fixed-order matrix)
Node 26.12 10.1 → 7.2 ms ~67.6k → ~70.1k (7.0 won 6 of 6 rounds)
Bun 1.4.4 20.5 → 19.4 ms ~59.8k → ~58.7k (within noise)

The trie router is about 2.5× faster than the cached sequential router when timing route lookups alone, and it doesn't need a cache. The CI benchmark now checks both routers against plain 0http. Node 26 is the fastest runtime measured.

Tests

  • Coverage: 100% of statements, branches, functions and lines. npm test now fails below 99%. Before this PR the threshold was 95% of lines only.
  • New test files:
    • specs/edge-cases.test.js covers start()/close() failures, stream errors, the promise depth limit, security header edge cases and config cloning.
    • specs/release-7.0.test.js covers the new 7.0 behavior.
    • specs/router-selection.test.js covers choosing between the two routers.
    • specs/bugfixes.test.js covers the fixes listed above.
  • TypeScript tests: they now check param inference, chaining and the readonly snapshot.
  • Runtimes: all 175 tests pass on Node 22.22, 24.22 and 26.12 and on Bun 1.4.4. Lint, the TypeScript check and the CI benchmark budgets pass, and npm audit --omit=dev reports 0 vulnerabilities.
  • Bun quirk: if a handler returns without ending the response, Bun 1.4.4 sends a 200 at the end of that tick even though res.headersSent still reads false. A stream that fails later therefore can't turn into a connection reset on Bun. The two affected tests check restana's own behavior (the response is destroyed) on every runtime and only check for the connection reset on Node.

Docs

  • MIGRATION.md: new upgrade guide from 6.x to 7.0.
  • CHANGELOG.md: new 7.0.0 entry.
  • docs/README.md:
    • a new Routers section;
    • a new TypeScript section;
    • an updated options list;
    • a 7.0 entry under Breaking changes.
  • Root README.md: 7.0 highlights.
  • CONTRIBUTING.md: the coverage requirement.
  • Landing page docs/index.html: v7.0 labels, new release cards, and the new router options in the configuration table.

Not included

  • prioRequestsProcessing still defaults to true, which measured faster than false in every round.
  • The package.json version is 7.0.0. Nothing has been published to npm or tagged.

Generated by Claude Code

claude and others added 7 commits October 22, 2026 16:08
- res.send(undefined, code) and promises resolving to undefined keep the explicit status code
- non-numeric error codes (e.g. 'ENOENT') no longer mask a valid err.statusCode
- APM instrumentation names multi-path routes per path
- expose runtime Protocol/Method objects matching the TypeScript enums
- type variadic use() middlewares; drop the unsupported prefix-less use(router)
- add .nvmrc

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2wFkaGAAWQhBw9VtCu7qe
- add router option ('trie' default, 'sequential' opt-out)
- add caseSensitive and ignoreTrailingSlash options for the trie router
- benchmark both routers in CI and route-scale suites
- document intentional trie vs sequential matching differences

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2wFkaGAAWQhBw9VtCu7qe
BREAKING CHANGES:
- the default errorHandler responds like res.send(err): standard status
  texts, debugErrors support, safe wrapping of non-Error values
- res.send(number) only treats integers from 100 to 999 as status codes;
  other numbers are serialized as JSON
- package exports map restricts imports to the public entry points
- TypeScript: chaining returns the receiver, readonly config snapshot and
  errorHandler, required Router.id
- trie router default (from the previous commit)

Also adds APM type declarations, a 6.x to 7.0 migration guide, and
updates the changelog, docs, README and landing page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2wFkaGAAWQhBw9VtCu7qe
- load the sequential router on demand (about 40% faster require, about 0.5MB less heap)
- avoid a per-response 'finish' listener in res.send() when no callback is given
- resolve security header options once per service
- infer req.params from route paths and expose RouteParams<Path>
- document TypeScript usage and the performance changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2wFkaGAAWQhBw9VtCu7qe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2wFkaGAAWQhBw9VtCu7qe
- cover lifecycle failures, stream error paths, promise depth cap,
  security header edge cases and configuration snapshot cloning
- fix res.send() with functions, symbols and bigints: respond with 500
  instead of treating a function as the res.end() callback (empty 200)
- enforce 99% statements, branches, functions and lines in npm test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2wFkaGAAWQhBw9VtCu7qe
Add release cards for typed route params, faster startup and response
fixes, and drop stale sequential-router and dependency-count claims.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jkyberneees
jkyberneees merged commit 89589a0 into master Oct 24, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants