Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
26
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# Changelog

## 7.0.0

Breaking release. See the [migration guide](MIGRATION.md).

### Breaking
- Upgraded `0http` to 5.2 and made its dependency-free trie router the default. Matching is
case-sensitive and stricter about route patterns. Set `caseSensitive: false` or
`router: 'sequential'` for the previous behavior. `routerCacheSize` only applies to the sequential router.
- The default `errorHandler` responds the same way as `res.send(err)`. The status text matches the
status code (`404 Not Found` instead of `404 Internal Server Error`), and `debugErrors` now applies
to `next(err)` and thrown errors. Non-`Error` values are wrapped safely.
- `res.send(number)` treats only integers from 100 to 999 as status codes. Other numbers are sent
as JSON bodies instead of being dropped.
- `package.json` `exports` limits imports to `restana`, `restana/libs/elastic-apm`,
`restana/libs/newrelic-apm` and `restana/package.json`.
- TypeScript: route registration and `use()` return the object they were called on.
`getConfigOptions()` returns `Readonly<Options>`, `errorHandler` is `readonly`, and
`use(router)` without a prefix is no longer accepted.

### Added
- `router` option to select the `'trie'` (default) or `'sequential'` router.
- `caseSensitive` and `ignoreTrailingSlash` options for the trie router.
- Runtime `restana.Protocol` and `restana.Method` objects matching the TypeScript enums.
- TypeScript support for registering multiple middlewares in one `use()` call.
- Type declarations for the Elastic APM and New Relic integrations.
- TypeScript route parameter inference: `service.get('/users/:id', (req) => req.params.id)` and the
`restana.RouteParams<Path>` helper type.
- `.nvmrc` pinning Node.js 26 (latest stable) for contributors; Node.js 24 LTS remains the minimum.

### Testing
- 100% statement, branch, function and line coverage, enforced at a 99% minimum in `npm test`.
- The test suite also passes on Bun.

### Performance
- The sequential router and its dependencies are loaded only when `router: 'sequential'` is selected,
cutting `require('restana')` time by about 40% and the initial heap by about 0.5 MB.
- `res.send()` no longer registers a `finish` listener per response when no callback is passed.
- Security header options are resolved once per service instead of on every request.

### Fixed
- `res.send(undefined, code)` and promises resolving to `undefined` no longer drop 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 instead of joining them (`GET /a,/b`).
- `res.send()` rejects function, symbol and bigint payloads with a `500` error. Functions used to be
treated as the `res.end()` callback, silently sending an empty `200` response.

## 6.1.0

### Added
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ npm ci
npm run check
```

`npm test` enforces at least 99% statement, branch, function and line coverage. New code
paths need tests, including failure paths.

Tests must listen on an ephemeral loopback port:

```js
Expand Down
157 changes: 157 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Migrating from restana 6.x to 7.0

Restana 7.0 makes the faster trie router the default, gives errors consistent
responses, and tightens the package's public API. Most applications upgrade by
changing the dependency version and checking the items below.

Requirements are unchanged: Node.js 24 or newer.

```bash
npm i restana@^7
```

## Quick checklist

| If your application… | Then… |
| --- | --- |
| relies on case-insensitive paths (`/Users` matching `/users`) | set `caseSensitive: false` |
| depends on the exact matching rules of the 6.x router | set `router: 'sequential'` |
| sets `routerCacheSize` | set `router: 'sequential'` too, or remove the option |
| asserts `message: 'Internal Server Error'` on non-500 errors | expect the standard status text, e.g. `'Not Found'` |
| calls `res.send(number)` with a number that is not an HTTP status code | expect it to be sent as a JSON body |
| imports internal modules such as `restana/libs/utils` | use only the public entry points listed below |
| uses the TypeScript declarations | see [TypeScript](#typescript) |

## Trie router is the default

Restana now uses the dependency-free [0http](https://github.com/BackendStack21/0http)
trie router (0http 5.2). Static routes resolve in constant time and dynamic
routes stay fast as the route count grows, with no route match cache to size.

The trie router intentionally differs from the 6.x sequential router:

- **Paths are case-sensitive.** `GET /Admin` no longer matches a route registered as `/admin`.
This agrees with how most reverse proxies, WAFs and auth layers classify paths.
- **Invalid patterns fail fast.** A `*` that is not the last segment, or a `?` that is not the
last character of a param segment, throws when the route is registered.
- **Absent optional params are omitted.** For `/users/:id?`, a request to `/users` leaves
`req.params.id` out instead of setting it to `undefined`.
- **Trailing wildcards match the bare prefix.** `/files/*` also matches `/files`, with
`req.params['*']` equal to `''`.
- **Empty route segments are ignored.** A route registered as `/a//b` is the same as `/a/b`.
- **Request paths must start with `/`.** Other request targets match nothing and get a 404.

New options control the trie router:

```js
const service = restana({
caseSensitive: false, // match '/Users' and '/users' (ASCII letters only); default: true
ignoreTrailingSlash: true // '/users/' is the same path as '/users'; default: true
})
```

To keep the 6.x behavior exactly, select the sequential router:

```js
const service = restana({ router: 'sequential', routerCacheSize: 2000 })
```

`routerCacheSize` only applies to the sequential router. The trie router ignores it.

## Error responses

The default `errorHandler` now responds the same way as `res.send(err)`. The
status text in the response matches the status code, and `debugErrors` applies to
errors passed to `next(err)` or thrown from handlers.

```js
service.get('/users/:id', async () => {
throw Object.assign(new Error('user 42 not in db'), { status: 404 })
})
```

| | 6.x | 7.0 |
| --- | --- | --- |
| default | `404 {"code":404,"message":"Internal Server Error"}` | `404 {"code":404,"message":"Not Found"}` |
| `debugErrors: true` (non-production) | `404 {"code":404,"message":"Internal Server Error"}` | `404 {"code":404,"message":"user 42 not in db"}` |
| `NODE_ENV=production` | `404 {"code":404,"message":"Internal Server Error"}` | `404 {"code":404,"message":"Not Found"}` |

Error details stay masked unless you opt in with `debugErrors: true`. They are
always masked when `NODE_ENV=production`.

Values passed to `next()` that are not `Error` instances, such as `next('oops')`,
are wrapped in an `Error`. The response is a generic
`500 Internal Server Error`, or uses the value's numeric `status`, `code` or
`statusCode`. With `debugErrors: true`, the original value is shown in `data`.

Custom `errorHandler` functions are not affected.

To keep the 6.x response body, provide your own handler:

```js
const service = restana({
errorHandler (err, req, res) {
const code = [err.status, err.code, err.statusCode].find(Number.isInteger) || 500
res.send({ code, message: 'Internal Server Error' }, code)
}
})
```

## `res.send()` with numbers

A number is treated as a status code only when it is an integer from 100 to 999.
Other numbers are sent as JSON instead of being dropped:

```js
res.send(404) // 404 with an empty body, unchanged
res.send(42.5) // 6.x: 200 with an empty body; 7.0: 200 with body 42.5 (application/json)
res.send(0, 201) // 6.x: 201 with an empty body; 7.0: 201 with body 0
```

`res.send(undefined, code)` now keeps the status code you pass. Previously it
was overwritten by `res.statusCode`.

## Package entry points

`package.json` now has an `exports` map. Only these entry points can be imported:

- `restana`
- `restana/libs/elastic-apm`
- `restana/libs/newrelic-apm`
- `restana/package.json`

Deep imports of internal modules, such as `restana/libs/utils` or
`restana/libs/response-extensions`, now fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
If you relied on one of them, copy the code you need or open an issue describing
your use case.

## TypeScript

- Route registration methods (`get`, `post`, …, `all`, `trace`) and `use()` now return the
object they were called on. Chaining on a router created by `newRouter()` now returns the
router, not a `Service`.
- `use()` accepts several middlewares: `service.use(cors, auth)` and
`service.use('/api', cors, auth)`.
- `use(router)` without a prefix is no longer accepted, because it threw at runtime. Mount
nested routers with a prefix: `service.use('/api', router)`.
- `getConfigOptions()` returns `Readonly<Options>`, and `service.errorHandler` is `readonly`.
Both already were at runtime.
- `Router.id` is always defined.
- `RegisterRoute<P, R = Service<P>>` takes an optional second type parameter for the return type.
- New options are typed: `router`, `caseSensitive` and `ignoreTrailingSlash`.
- Route params are inferred from the path: in `service.get('/users/:id', handler)`, `req.params.id`
is typed and autocompleted. Any other key is still typed as `string`, so existing code compiles
unchanged. `Request`, `RequestHandler` and `RouteParams` take an optional params type argument.
- `restana.Protocol` and `restana.Method` now exist at runtime, so `restana.Protocol.HTTP2`
works in value positions too.
- `restana/libs/elastic-apm` and `restana/libs/newrelic-apm` now ship type declarations.

## Other fixes

- Error status codes are taken from the first valid value of `err.status`, `err.code` or
`err.statusCode`. A Node.js system error code such as `'ENOENT'` no longer hides a valid
`statusCode`.
- APM integrations name routes registered with several paths, `service.get(['/a', '/b'], …)`,
one path at a time (`GET /a`, `GET /b`) instead of joining them (`GET /a,/b`).
- `res.send()` with a function, symbol or bigint now responds with a `500` error. A function used
to be called as the `res.end()` callback, sending an empty `200` response.
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ http.createServer(service).listen(3000, '0.0.0.0')

# Security defaults
Restana ships with secure defaults out of the box:
- **Error handling**: The default error handler returns a generic `Internal Server Error` message, preventing internal details (stack traces, database errors, file paths) from leaking to clients. Provide a custom `errorHandler` to control what gets exposed.
- **Error handling**: The default error handler only returns the status code and its standard status text (e.g. `Not Found`), preventing internal details (stack traces, database errors, file paths) from leaking to clients. Provide a custom `errorHandler` to control what gets exposed.
- **Stream safety**: Stream errors are handled gracefully, preventing connection leaks.
- **Immutable config**: `getConfigOptions()` returns a frozen copy, preventing middleware from mutating internal framework options.
- **Response headers**: Browser hardening headers are enabled by default, and connection-specific or cookie headers cannot be injected through `res.send()`.
Expand All @@ -77,16 +77,19 @@ const service = restana({ trustProxy: true })

Error details are hidden by default in every environment. For local debugging only, opt in with `debugErrors: true`; production mode always masks details.

## 6.1 highlights
- Reliable `start()` rejection on port and socket errors.
- Isolated, deeply frozen configuration snapshots.
- Boolean response bodies and array-valued headers.
- Correct `routerCacheSize: 0` behavior.
- Updated TypeScript API, reproducible installs, and route-scaling benchmarks.
- Expanded security and performance regression coverage.
## 7.0 highlights
- Dependency-free trie router by default: constant-time static routes and no match cache to tune.
- Case-sensitive routing that agrees with reverse proxies and WAFs (`caseSensitive: false` to opt out).
- Consistent error responses from `res.send(err)` and the default error handler.
- Faster startup: no third-party modules are loaded by default.
- TypeScript route params inferred from the path (`'/users/:id'` → `req.params.id`).
- Stricter `res.send()` status code handling and a typed, explicit public API.

Upgrading from 6.x? Read the [migration guide](MIGRATION.md).

# More
- Website and documentation: https://restana.21no.de
- [Full API guide](docs/README.md)
- [Changelog](CHANGELOG.md)
- [Migration guide (6.x → 7.0)](MIGRATION.md)
- [Contributing](CONTRIBUTING.md)
Loading
Loading