express-fastify-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/changelog/README.md +5 -0
  4. package/changelog/log-2025-02-07.md +10 -0
  5. package/changelog/log-2026-06-25.md +151 -0
  6. package/dist/app/ExpressLikeApp.d.ts +9 -0
  7. package/dist/app/ExpressLikeApp.d.ts.map +1 -0
  8. package/dist/app/ExpressLikeApp.js +64 -0
  9. package/dist/app/ExpressLikeApp.js.map +1 -0
  10. package/dist/app/RouteStore.d.ts +17 -0
  11. package/dist/app/RouteStore.d.ts.map +1 -0
  12. package/dist/app/RouteStore.js +43 -0
  13. package/dist/app/RouteStore.js.map +1 -0
  14. package/dist/app/classify.d.ts +8 -0
  15. package/dist/app/classify.d.ts.map +1 -0
  16. package/dist/app/classify.js +25 -0
  17. package/dist/app/classify.js.map +1 -0
  18. package/dist/app/compile.d.ts +25 -0
  19. package/dist/app/compile.d.ts.map +1 -0
  20. package/dist/app/compile.js +195 -0
  21. package/dist/app/compile.js.map +1 -0
  22. package/dist/app/flattenRouter.d.ts +51 -0
  23. package/dist/app/flattenRouter.d.ts.map +1 -0
  24. package/dist/app/flattenRouter.js +126 -0
  25. package/dist/app/flattenRouter.js.map +1 -0
  26. package/dist/app/introspectExpress.d.ts +29 -0
  27. package/dist/app/introspectExpress.d.ts.map +1 -0
  28. package/dist/app/introspectExpress.js +60 -0
  29. package/dist/app/introspectExpress.js.map +1 -0
  30. package/dist/examples/auth.d.ts +6 -0
  31. package/dist/examples/auth.d.ts.map +1 -0
  32. package/dist/examples/auth.js +26 -0
  33. package/dist/examples/auth.js.map +1 -0
  34. package/dist/examples/logging.d.ts +6 -0
  35. package/dist/examples/logging.d.ts.map +1 -0
  36. package/dist/examples/logging.js +25 -0
  37. package/dist/examples/logging.js.map +1 -0
  38. package/dist/examples/uploads.d.ts +6 -0
  39. package/dist/examples/uploads.d.ts.map +1 -0
  40. package/dist/examples/uploads.js +21 -0
  41. package/dist/examples/uploads.js.map +1 -0
  42. package/dist/express/engine.d.ts +6 -0
  43. package/dist/express/engine.d.ts.map +1 -0
  44. package/dist/express/engine.js +14 -0
  45. package/dist/express/engine.js.map +1 -0
  46. package/dist/express/middleware.d.ts +15 -0
  47. package/dist/express/middleware.d.ts.map +1 -0
  48. package/dist/express/middleware.js +32 -0
  49. package/dist/express/middleware.js.map +1 -0
  50. package/dist/express/mount.d.ts +35 -0
  51. package/dist/express/mount.d.ts.map +1 -0
  52. package/dist/express/mount.js +78 -0
  53. package/dist/express/mount.js.map +1 -0
  54. package/dist/fastify/adapters/middleware.d.ts +8 -0
  55. package/dist/fastify/adapters/middleware.d.ts.map +1 -0
  56. package/dist/fastify/adapters/middleware.js +29 -0
  57. package/dist/fastify/adapters/middleware.js.map +1 -0
  58. package/dist/fastify/adapters/request.d.ts +19 -0
  59. package/dist/fastify/adapters/request.d.ts.map +1 -0
  60. package/dist/fastify/adapters/request.js +258 -0
  61. package/dist/fastify/adapters/request.js.map +1 -0
  62. package/dist/fastify/adapters/response.d.ts +19 -0
  63. package/dist/fastify/adapters/response.d.ts.map +1 -0
  64. package/dist/fastify/adapters/response.js +667 -0
  65. package/dist/fastify/adapters/response.js.map +1 -0
  66. package/dist/fastify/register.d.ts +12 -0
  67. package/dist/fastify/register.d.ts.map +1 -0
  68. package/dist/fastify/register.js +15 -0
  69. package/dist/fastify/register.js.map +1 -0
  70. package/dist/index.d.ts +13 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +18 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/runtime/decorators.d.ts +7 -0
  75. package/dist/runtime/decorators.d.ts.map +1 -0
  76. package/dist/runtime/decorators.js +17 -0
  77. package/dist/runtime/decorators.js.map +1 -0
  78. package/dist/runtime/errorHandler.d.ts +13 -0
  79. package/dist/runtime/errorHandler.d.ts.map +1 -0
  80. package/dist/runtime/errorHandler.js +95 -0
  81. package/dist/runtime/errorHandler.js.map +1 -0
  82. package/dist/runtime/expressLane.d.ts +40 -0
  83. package/dist/runtime/expressLane.d.ts.map +1 -0
  84. package/dist/runtime/expressLane.js +71 -0
  85. package/dist/runtime/expressLane.js.map +1 -0
  86. package/dist/runtime/fast.d.ts +43 -0
  87. package/dist/runtime/fast.d.ts.map +1 -0
  88. package/dist/runtime/fast.js +150 -0
  89. package/dist/runtime/fast.js.map +1 -0
  90. package/dist/runtime/lifecycle.d.ts +10 -0
  91. package/dist/runtime/lifecycle.d.ts.map +1 -0
  92. package/dist/runtime/lifecycle.js +152 -0
  93. package/dist/runtime/lifecycle.js.map +1 -0
  94. package/dist/runtime/populateExpress.d.ts +7 -0
  95. package/dist/runtime/populateExpress.d.ts.map +1 -0
  96. package/dist/runtime/populateExpress.js +27 -0
  97. package/dist/runtime/populateExpress.js.map +1 -0
  98. package/dist/types/express.d.ts +97 -0
  99. package/dist/types/express.d.ts.map +1 -0
  100. package/dist/types/express.js +12 -0
  101. package/dist/types/express.js.map +1 -0
  102. package/dist/types/internal.d.ts +60 -0
  103. package/dist/types/internal.d.ts.map +1 -0
  104. package/dist/types/internal.js +7 -0
  105. package/dist/types/internal.js.map +1 -0
  106. package/dist/utils/assert.d.ts +6 -0
  107. package/dist/utils/assert.d.ts.map +1 -0
  108. package/dist/utils/assert.js +17 -0
  109. package/dist/utils/assert.js.map +1 -0
  110. package/dist/utils/detect.d.ts +14 -0
  111. package/dist/utils/detect.d.ts.map +1 -0
  112. package/dist/utils/detect.js +64 -0
  113. package/dist/utils/detect.js.map +1 -0
  114. package/dist/utils/maxListeners.d.ts +28 -0
  115. package/dist/utils/maxListeners.d.ts.map +1 -0
  116. package/dist/utils/maxListeners.js +50 -0
  117. package/dist/utils/maxListeners.js.map +1 -0
  118. package/dist/utils/patchRouterLayer.d.ts +12 -0
  119. package/dist/utils/patchRouterLayer.d.ts.map +1 -0
  120. package/dist/utils/patchRouterLayer.js +96 -0
  121. package/dist/utils/patchRouterLayer.js.map +1 -0
  122. package/dist/utils/path.d.ts +6 -0
  123. package/dist/utils/path.d.ts.map +1 -0
  124. package/dist/utils/path.js +23 -0
  125. package/dist/utils/path.js.map +1 -0
  126. package/dist/utils/runtimeLogger.d.ts +15 -0
  127. package/dist/utils/runtimeLogger.d.ts.map +1 -0
  128. package/dist/utils/runtimeLogger.js +28 -0
  129. package/dist/utils/runtimeLogger.js.map +1 -0
  130. package/dist/utils/unwrap.d.ts +12 -0
  131. package/dist/utils/unwrap.d.ts.map +1 -0
  132. package/dist/utils/unwrap.js +24 -0
  133. package/dist/utils/unwrap.js.map +1 -0
  134. package/package.json +94 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 express-fastify-runtime contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # express-fastify-runtime
2
+
3
+ > Your Express app. Fastify's speed. One line. No rewrite.
4
+
5
+ ```ts
6
+ import { fast } from "express-fastify-runtime";
7
+ import express from "express";
8
+
9
+ const app = express();
10
+ app.get("/", (req, res) => res.json({ hello: "world" }));
11
+
12
+ fast(app).listen({ port: 3000 }); // 👈 that's the whole trick
13
+ ```
14
+
15
+ ---
16
+
17
+ ## A short, slightly emotional story
18
+
19
+ I love Express. I love it the way you love a comfortable pair of shoes — `req`, `res`,
20
+ `next`, a thousand middlewares on npm, and muscle memory built over years. Express is *home*.
21
+
22
+ Then one day someone showed me a Fastify benchmark and my coffee went cold. "Two-ish times the
23
+ throughput," they said, smiling like they'd discovered fire. And I thought: do I really have to
24
+ abandon my comfortable shoes and rewrite everything in a new framework just to go faster?
25
+
26
+ So I went looking for a shortcut. I found tools that "run Express on Fastify" — and many of them
27
+ do *exactly* one thing: they hand your Express app to Fastify and let Fastify… call Express for
28
+ every request. Your app technically runs "on Fastify." It is not one bit faster. It's a very
29
+ polite handshake between two frameworks where nothing actually changes. Cool sticker, no engine.
30
+
31
+ **`express-fastify-runtime` is the engine.** It doesn't just *expose* your Express app to
32
+ Fastify — it **compiles** your safe routes and middleware onto Fastify's real request pipeline,
33
+ and only falls back to actual Express for the things that genuinely need it (multipart uploads,
34
+ `res.render`, streaming bodies, anything unusual). You keep writing Express. It actually gets
35
+ fast.
36
+
37
+ You don't rewrite a thing. You wrap one line. Your shoes stay on.
38
+
39
+ ---
40
+
41
+ ## What you get
42
+
43
+ - **It's still Express.** `app.use`, `app.get`, `req`, `res`, `next`, your middleware, your
44
+ routers, your error handlers. Nothing to relearn. **Express 4 and 5 both welcome.**
45
+ - **It's actually fast.** Safe routes run compiled on Fastify — faster than plain Express across
46
+ the board, and matching or beating Fastify on middleware-heavy and small-payload workloads
47
+ (see [Benchmarks](#benchmarks)).
48
+ - **Nothing breaks.** Anything that can't safely run on Fastify is transparently handled by a
49
+ real embedded `express()` instance. No silent behavior changes; morgan logs, helmet headers,
50
+ auth, cookies, JSON parsing, streaming/SSE, and error middleware all behave like Express.
51
+ - **It's a real Fastify instance.** `fast(app)` returns a `FastifyInstance`, so Fastify fans get
52
+ their plugins, hooks, and the raw Node server for WebSockets/Socket.IO.
53
+
54
+ ---
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ npm install express-fastify-runtime
60
+ ```
61
+
62
+ `express` is a **peer dependency** — bring your own (`^4.18` or `^5`). `fast()` uses whatever
63
+ Express you already have.
64
+
65
+ > **One rule:** import `express-fastify-runtime` **before** you create your Express app or any
66
+ > `express.Router()`. It patches the router layer at load time so middleware mounted with a path
67
+ > (`app.use('/api', mw)`, `router.use('/x', mw)`) can be compiled onto the Fastify lane. Import it
68
+ > late and those bits still work — they just fall back to the (slower) Express lane.
69
+
70
+ ---
71
+
72
+ ## For Express fans — wrap what you already have
73
+
74
+ You wrote a normal Express app. Wrap it. Done.
75
+
76
+ ```ts
77
+ import "express-fastify-runtime"; // load first (the one rule)
78
+ import express from "express";
79
+ import morgan from "morgan";
80
+ import helmet from "helmet";
81
+ import { fast } from "express-fastify-runtime";
82
+
83
+ const app = express();
84
+ app.use(morgan("tiny")); // logs, correctly, per request
85
+ app.use(helmet()); // security headers, intact
86
+ app.use(express.json()); // parsed by Fastify's fast parser under the hood
87
+
88
+ app.get("/users/:id", (req, res) => {
89
+ res.json({ id: req.params.id });
90
+ });
91
+
92
+ app.use((err, req, res, next) => { // your error middleware still works
93
+ res.status(500).json({ error: err.message });
94
+ });
95
+
96
+ const fastApp = fast(app); // returns a Fastify instance
97
+ fastApp.listen({ port: 3000 });
98
+ ```
99
+
100
+ Routers, controllers, `req.body`, `req.query`, `req.params`, `res.status().json()`,
101
+ `res.redirect()`, `res.cookie()`, async handlers, `next(err)` — all the Express you already
102
+ write. No changes.
103
+
104
+ ## For Fastify fans — it's a real Fastify instance
105
+
106
+ `fast(app)` hands you back a genuine `FastifyInstance`. Add plugins, register hooks, use the
107
+ Fastify ecosystem — your Express routes just ride along on the Fastify lane.
108
+
109
+ ```ts
110
+ import "express-fastify-runtime";
111
+ import express from "express";
112
+ import rateLimit from "@fastify/rate-limit";
113
+ import { fast } from "express-fastify-runtime";
114
+
115
+ const app = express();
116
+ app.get("/", (req, res) => res.json({ ok: true }));
117
+
118
+ const fastApp = fast(app, { fastify: { logger: true } }); // pass Fastify options through
119
+ await fastApp.register(rateLimit, { max: 100 }); // real Fastify plugins
120
+ await fastApp.ready(); // let plugins load before listen
121
+ await fastApp.listen({ port: 3000 });
122
+ ```
123
+
124
+ ## Plain Node `http`
125
+
126
+ `fastApp.server` is the real Node HTTP server, and `server.listen(...)` is wired to run
127
+ Fastify's full lifecycle (so 404s and internals work):
128
+
129
+ ```ts
130
+ const fastApp = fast(app);
131
+ const server = fastApp.server; // http.Server
132
+ server.listen(3000, () => console.log("up on 3000"));
133
+ ```
134
+
135
+ ## Socket.IO / WebSockets
136
+
137
+ Because you can reach the underlying HTTP server, real-time works exactly like it does in any
138
+ Node app — attach your socket server to `fastApp.server`:
139
+
140
+ ```ts
141
+ import { Server as IOServer } from "socket.io";
142
+
143
+ const fastApp = fast(app);
144
+ const io = new IOServer(fastApp.server); // share the same HTTP server
145
+ io.on("connection", (socket) => socket.emit("hello", "from express-fastify-runtime"));
146
+
147
+ fastApp.server.listen(3000);
148
+ ```
149
+
150
+ The same pattern works for `ws`, `@fastify/websocket`, or anything that takes an
151
+ `http.Server`.
152
+
153
+ ## Controlling which lane a route runs on
154
+
155
+ Every request runs on one of two lanes ([how it works](#how-it-works-30-seconds)):
156
+
157
+ - **Fastify lane (default, fast)** — safe routes and middleware are compiled onto Fastify
158
+ automatically. You don't annotate anything for the common case.
159
+ - **Express lane (real Express)** — anything unsafe (multipart/uploads, `res.render`, `res.sendFile`,
160
+ stream-piping middleware) is detected and transparently handled by the embedded real Express app.
161
+
162
+ When detection can't be sure, it already errs toward the Express lane. The two helpers below let you
163
+ **force the Express lane** for a specific route when you want Express-only behavior guaranteed
164
+ (e.g. a view engine) without relying on detection.
165
+
166
+ ### `expressLane(fn)` — force the Express lane (works anywhere)
167
+
168
+ Wrap the handler/middleware. Works with plain functions and arrow functions:
169
+
170
+ ```ts
171
+ import { expressLane } from "express-fastify-runtime";
172
+
173
+ app.get("/page", expressLane((req, res) => res.render("index", { title: "Hi" })));
174
+ ```
175
+
176
+ ### `@ExpressLane` — decorator form (class-method controllers)
177
+
178
+ Requires `"experimentalDecorators": true` in your `tsconfig.json`. The Express-lane marker lives on
179
+ the decorated method itself — register that method **directly** as the handler; don't `.bind()` it
180
+ or wrap it in an arrow first (that creates a new function and drops the marker — use `expressLane()`
181
+ for those cases).
182
+
183
+ ```ts
184
+ import { ExpressLane } from "express-fastify-runtime";
185
+
186
+ class PageController {
187
+ @ExpressLane
188
+ page(req, res) {
189
+ res.render("index", { title: "Hi" });
190
+ }
191
+ }
192
+
193
+ const pages = new PageController();
194
+ app.get("/page", pages.page); // the decorated method carries the Express-lane marker
195
+ ```
196
+
197
+ Everything else stays on the fast lane.
198
+
199
+ ---
200
+
201
+ ## How it works (30 seconds)
202
+
203
+ ```
204
+ your Express app (unchanged)
205
+ │ compiled once, at startup
206
+ ┌─────────────┴──────────────┐
207
+ ▼ ▼
208
+ Fastify lane Embedded Express lane
209
+ (safe routes & middleware (real express() instance —
210
+ compiled onto Fastify) uploads, res.render, streams…)
211
+ ```
212
+
213
+ A request is matched by Fastify. If it's a compiled (safe) route, it runs on Fastify's pipeline
214
+ with a thin Express-compatible `req`/`res`. If it isn't, Fastify hands the raw request to the
215
+ embedded real Express app. **When in doubt, it uses real Express** — it never guesses and
216
+ silently changes behavior.
217
+
218
+ Lane classification, body parsing (`express.json()` → Fastify's parser), router flattening, and
219
+ error-handler wiring all happen **once at startup**. There's no per-request framework juggling.
220
+
221
+ ---
222
+
223
+ ## Benchmarks
224
+
225
+ Numbers are req/s, **median of repeated runs with warmup** (single-shot HTTP benchmarks swing
226
+ 20–30%, so don't trust one sample — including ours; reproduce with `npm run benchmark:table`).
227
+ 10 connections, Node on an Apple-silicon laptop. Higher is better.
228
+
229
+ | Scenario | Express | Fastify | **fast()** | fast/Express | fast/Fastify |
230
+ |---|--:|--:|--:|--:|--:|
231
+ | Plain JSON route (5 middleware) | 39,976 | 68,112 | **50,552** | **1.26×** | 0.74× |
232
+ | JSON DB read | 41,848 | 68,624 | **50,584** | **1.21×** | 0.74× |
233
+ | Middleware stack (helmet+morgan+json+custom) | 29,424 | 32,736 | **40,184** | **1.37×** | **1.23×** |
234
+ | POST 1KB JSON | 33,018 | 46,752 | **46,696** | **1.41×** | **1.00×** |
235
+ | Auth (JWT verify) | 17,636 | 33,976 | **19,360** | **1.10×** | 0.57× |
236
+
237
+ **Reading the table:**
238
+
239
+ - `fast()` is **faster than plain Express** across the board — same code, more throughput.
240
+ - It **matches or beats Fastify** on middleware-heavy and small-payload workloads.
241
+ - On a tight pure-JSON hot path it trails raw Fastify a little — that's the unavoidable cost of
242
+ presenting a real Express `req`/`res`, and it's still well ahead of Express.
243
+ - **Auth (JWT):** the gap there isn't the adapter — it's the *library*. Fastify's benchmark uses
244
+ `@fastify/jwt` (built on `fast-jwt`), which is simply faster than `jsonwebtoken`. Want that
245
+ speed in your Express app? Use `fast-jwt` directly in your auth middleware — it's
246
+ framework-agnostic.
247
+ - **Uploads** (multipart) run on the Express lane by design (multer), so Fastify's native
248
+ `@fastify/multipart` wins there — expected, not a regression.
249
+
250
+ Run them yourself:
251
+
252
+ ```bash
253
+ npm run benchmark:table # the table above (warmup + median)
254
+ npm run benchmark:fast-vs-fastify # fast() vs plain Fastify, MW=0 and MW=5
255
+ npm run benchmark # full suite: express / fastify / node-http / runtime
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Compatibility & guarantees
261
+
262
+ - **Express 4 → 5 upward.** Legacy apps and modern apps. The goal is that adding `fast()`
263
+ changes your throughput, not your behavior.
264
+ - **Concurrency-safe.** Every in-flight request gets its own `req`/`res` — heavily tested under
265
+ concurrent async load (no cross-talk, correct morgan logs per request).
266
+ - **Supported:** `app.use` / `app.METHOD` / `app.all`, `express.Router()` (flattened when safe),
267
+ `req.body|query|params|headers|cookies`, `res.status|json|send|sendStatus|set|get|redirect|cookie|type|end`,
268
+ streaming via `res.write`/`res.end`, async handlers, global `(err, req, res, next)` error
269
+ middleware, `express.json()`/`urlencoded`.
270
+ - **Express lane (real Express):** multipart/uploads, `res.render` + view engines,
271
+ `res.sendFile`, stream-based middleware, RegExp route paths — or anything you mark with
272
+ `expressLane()`.
273
+
274
+ **Middleware semantics:** the chain is continuation-based, exactly like Express's router —
275
+ `next()` advances the chain however it's called: synchronously, from an `async/await` handler,
276
+ or from a detached callback / `setTimeout` / promise (classic callback-style middleware). A
277
+ middleware that ends the response without calling `next()` stops the chain. The only thing that
278
+ "hangs" is a handler that never calls `next()` and never responds — which hangs in real Express
279
+ too.
280
+
281
+ ---
282
+
283
+ ## Contributing
284
+
285
+ PRs welcome — the bar is "it stays Express-correct **and** fast."
286
+
287
+ ```bash
288
+ npm install
289
+ npm run build # tsc → dist (CommonJS)
290
+ npm test # unit + integration (incl. concurrency / parity / error-handling)
291
+ npm run benchmark:table
292
+ ```
293
+
294
+ Rules of the road:
295
+
296
+ 1. **Correctness beats speed, always.** Adding `fast()` must never change Express behavior. If a
297
+ perf idea risks that, it doesn't ship.
298
+ 2. **No shared per-request state.** Each request gets its own `req`/`res`. Never reintroduce a
299
+ single mutated adapter object — it corrupts concurrent async requests. Validate every
300
+ perf change with concurrent **async** handlers (`test/integration/concurrency.test.js`).
301
+ 3. **Imports are CommonJS, no file extensions** (matches the current `tsconfig`).
302
+ 4. **Add a test** for any behavior you touch; keep the full suite green (`npm test`).
303
+ 5. **Measure with `npm run benchmark:table`** (warmup + median), not single-shot runs.
304
+ 6. Update `docs/` and `changelog/log-YYYY-MM-DD.md` for notable changes.
305
+
306
+ See `docs/` for deeper notes: `SPEC.md`, `HOW_EXPRESS_LANE_WORKS.md`,
307
+ `FAST_PRODUCTION_CHECKLIST.md`, `OPTIMIZATION.md`, `EXPRESS_FEATURES.md`.
308
+
309
+ ## Reporting issues
310
+
311
+ Found a bug or have a feature request? Open an issue on
312
+ **[GitHub Issues](https://github.com/John-Daniels/express-fastify-runtime/issues)**.
313
+
314
+ To help us reproduce quickly, please include:
315
+
316
+ - **Versions** — `express-fastify-runtime`, `express` (4 or 5), `fastify`, and Node.
317
+ - **Lane** — run `fast(app, { experimental: { diagnostics: true } })` and note whether the failing
318
+ request logs `Fastify lane` or `Express lane`.
319
+ - **A minimal repro** — the smallest app/route that triggers it, plus the full error stack (not just
320
+ the message) and the request (method, path, content-type, body).
321
+
322
+ Security issue? Please report it privately to the maintainer rather than opening a public issue.
323
+
324
+ ## License
325
+
326
+ MIT
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ One log file per day worked on the project. Format: `log-YYYY-MM-DD.md` (human and AI friendly).
4
+
5
+ - **log-YYYY-MM-DD.md** — Short bullet list of changes that day.
@@ -0,0 +1,10 @@
1
+ # 2025-02-07
2
+
3
+ - Dev fallback logger: warn when Router falls back to Express lane (middleware/RegExp path). `createApp({ dev: true })` or `NODE_ENV !== 'production'`.
4
+ - Changelog folder: `changelog/log-YYYY-MM-DD.md` per day.
5
+ - NPM publish prep: repository, license, keywords, files in package.json.
6
+ - compile.ts: kept current version (nextCalled for error/flow clarity); removed compile.old.ts.
7
+ - EXPRESS_FEATURES.md: added Fallback behavior section, createApp(options), clarified supported vs not yet.
8
+ - docs/EDGE_CASES.md: added edge-case checklist (Tier 1–3, production guarantees, benchmark ideas).
9
+ - **fast(app, ops):** New API to compile an existing Express app onto Fastify. Introspects `app.router.stack`, classifies and registers safe routes on Fastify, mounts Express for fallback. Returns real Fastify instance. Requires loading express-fastify-runtime before creating the app (for Layer _path). See `src/runtime/fast.ts`, `src/app/introspectExpress.ts`.
10
+ - **Benchmark notes:** Documented in benchmarks/README.md why Fastify wins on uploads (multer vs native multipart) and auth (native JWT plugin vs userland); payloads and CRUD are within target.
@@ -0,0 +1,151 @@
1
+ # 2026-06-25
2
+
3
+ ## Publish prep
4
+
5
+ - **Fastify upgraded to 5.8.5** (latest; includes the recent security/CVE fixes). Dependency floor
6
+ raised to `^5.8.5`. We use only stable Fastify-5 APIs and the one 5.x behavior change (stricter
7
+ RFC-9110 content-type parsing, 5.7.2) predates our prior 5.7.4 — full test suite stays green on
8
+ **Express 4 and 5** after the bump.
9
+ - **Single self-discovering test command**: `npm test` now runs `node --test test/*/*.test.js`
10
+ (shell-expanded; works on every Node version) instead of a hand-maintained file list; added
11
+ `test:compat`. New `test/<category>/*.test.js` files are picked up automatically.
12
+ - **Packaging**: added `publishConfig.access = "public"`; verified `npm pack` (64 kB, dist + README
13
+ + LICENSE + changelog) consumes cleanly from CJS (`require`) and ESM (`import`), Express not
14
+ bundled (peer dep). `npm publish` is run by the maintainer (`npm login` required).
15
+
16
+ ## Fix: APM-instrumented apps (Sentry/OpenTelemetry), string ports, wrapped body parsers
17
+
18
+ Found while running a real Express 4 app (Sentry + helmet + morgan + hpp) through `fast()`:
19
+
20
+ - **`server.listen(process.env.PORT, cb)` bound a random port.** `process.env.PORT` is a string;
21
+ our wrapper coerced non-numbers to 0 (OS-assigned), so the server logged the intended port but
22
+ was unreachable. `runtime/fast.ts` now coerces string ports (and `{ port: "3000" }`) like
23
+ Node/Express.
24
+ - **Helmet crashed with "Cannot set properties of undefined (setting 'content-security-policy')".**
25
+ Sentry's OpenTelemetry instrumentation wraps Express handlers, so Express 4's `expressInit` (which
26
+ reassigns req/res prototypes) had `handle.name === "patched"` and slipped past our skip — it ran
27
+ on the Fastify lane and broke the response object. We now skip `query`/`expressInit` by
28
+ **`layer.name`** (Express preserves it through APM wrapping), in `app/flattenRouter.ts`.
29
+ - **`express.json()` crashed with "argument stream must be a stream" on POST routes.** The wrapped
30
+ `jsonParser` wasn't recognized (so it wasn't mapped to Fastify's parser) and tried to read the
31
+ already-consumed request stream. We now identify `express.json` by `layer.name` and replace it
32
+ with a no-op on the Fastify lane (Fastify provides `req.body`); `utils/detect.ts` and
33
+ `express/middleware.ts` also unwrap APM wrappers (`__original`) as a secondary safety. New
34
+ `test/integration/instrumentation.test.js` simulates the wrapping.
35
+ - `express.urlencoded()`/`raw()`/`text()` continue to route to the Express lane (Fastify has no
36
+ parser for them); `express.json()` stays fast on Fastify. Verified end-to-end against the real
37
+ Express 4 app: `GET /`, `POST /v1/user/login` (JSON), and a duplicate urlencoded webhook route
38
+ all work, no crashes.
39
+
40
+ ## Fix: repeated route registrations + stream body parsers
41
+
42
+ - **"Method 'POST' already declared" crash fixed.** Declaring the same method+path more than once
43
+ is valid Express (`router.post('/x', parser); router.post('/x', handler)` — the layers chain via
44
+ `next()`), but Fastify forbids duplicate method+path, so we crashed at startup. `compile.ts` now
45
+ counts route entries per concrete method+path (across both lanes, expanding `all`) and registers
46
+ a method on the Fastify lane only when it's declared exactly once; any method+path declared >1
47
+ time (incl. `all`+method overlaps, or a same-path Express-lane entry) defers entirely to the real
48
+ embedded Express app, which runs every layer in order. Single-route hot path unchanged.
49
+ - **Stream body parsers routed to the Express lane.** `detect.ts` now treats `express.urlencoded`
50
+ (`urlencodedParser`), `express.raw` (`rawParser`), and `express.text` (`textParser`) as
51
+ Express-required — they consume the raw request stream, which Fastify has already drained and has
52
+ no parser for, so `req.body` would be empty on the Fastify lane. (`express.json` is unchanged: it
53
+ stays mapped to Fastify's native JSON parser.) Fixes both the duplicate case and a single inline
54
+ `app.post('/x', express.urlencoded(...), handler)`.
55
+ - New `test/integration/duplicate-routes.test.js` (incl. the exact reported WhatsApp pattern);
56
+ verified against a real Express 4 consumer: no crash, 200, body parsed.
57
+
58
+ ## Production-readiness pass (npm-ready, Express 4→5, freaking fast)
59
+
60
+ - **`fast()` no longer couples its types to a specific `@types/express`.** Its parameter is now a
61
+ structural `ExpressApp` type (a request-handler function with Express's methods), exported from
62
+ the package, instead of `Application` from `@types/express`. A host project on Express 4 (or any
63
+ @types/express version that differs from the one this package was built with) previously got
64
+ `TS2345: 'Express' is not assignable to 'Application'` (PathParams/RegExp vs string). Verified:
65
+ `fast(express())` type-checks under a real Express-4 (`@types/express@4`) consumer.
66
+
67
+ - **Express 4 support.** `fast()` previously crashed on Express 4 (its `app.router` getter
68
+ throws; the router is `app._router`) and Express 4 injects `query`/`expressInit` middleware
69
+ into the app router stack — `expressInit` reassigns `req`/`res` prototypes and broke our
70
+ adapter. Fixes: `introspectExpress.ts` reads `app._router` first (guarded), `patchRouterLayer.ts`
71
+ also patches Express 4's bundled `express/lib/router/layer`, and `flattenRouter.ts` skips the
72
+ `query`/`expressInit` built-ins. **The full test suite passes under Express 4.22 and Express 5.2.**
73
+ - **`express` is now a peerDependency** (`^4.18 || ^5`) instead of a hard dependency — consumers
74
+ bring their own Express; nothing is bundled/forced.
75
+ - **Error handling parity.** Un-skipped the Fastify-lane error test; `next(err)`/throw/async
76
+ rejection reach Express 4-arg error middleware on both `fast()` and `createApp()`
77
+ (`createApp` now wires `setErrorHandler`). 4-arg error middleware is excluded from the normal
78
+ chain. New `test/integration/error-handling.test.js`.
79
+ - **Streaming / SSE.** Added `res.write`/`res.writeHead`/`res.flushHeaders` (and hijack-aware
80
+ `res.end`) to the response adapter via `reply.hijack()`, so streaming and Server-Sent Events
81
+ work on the Fastify lane. Covered (incl. under concurrency) in new
82
+ `test/integration/parity.test.js` alongside redirect, send variants, 204, 404 fall-through,
83
+ and 1MB echo.
84
+ - **npm packaging:** added an `exports` map (types/require/import); verified `npm pack` consumes
85
+ cleanly from both CJS (`require`) and ESM (`import`) apps, with Express not bundled.
86
+ - **Benchmarks:** new `benchmark:table` (warmup + interleaved median, Markdown output). Median
87
+ numbers: fast() is ~1.2–1.5× plain Express across the board and ~1.1–1.3× Fastify on
88
+ middleware-heavy / small-payload workloads; ~0.7× Fastify on a pure-JSON hot path (the cost of
89
+ a real Express req/res). README rewritten (story-driven) with the table and Socket.IO/http/
90
+ Fastify-plugin examples.
91
+ - **Continuation-based middleware chain (correctness, kept fast).** Replaced the
92
+ "advance synchronously or await the returned promise" runner with a continuation runner that
93
+ matches Express's router: `next()` advances the chain however it's called — sync, `async/await`,
94
+ or a **detached callback / setTimeout / microtask** (classic callback-style middleware, e.g.
95
+ `db.query(..., () => next())`). Previously such middleware silently broke the chain on the fast
96
+ lane. A handler that ends the response without `next()` stops the chain; errors (sync throw,
97
+ `next(err)`, async rejection) reach Fastify's error handler. The runner is **sync-first** — it
98
+ returns synchronously (no Promise) when the chain settles synchronously, and the single
99
+ no-`next` handler hot path bypasses it entirely — so the benchmarks held (fast() stays
100
+ ~1.1–1.4× Express on every scenario and ≥1.0× Fastify on middleware-stack and payloads). New
101
+ `test/integration/middleware-chain.test.js`. This removes the previously-documented
102
+ "detached next()" limitation.
103
+ - **CI:** GitHub Actions matrix (Node 18/20/22 × Express 4/5) + benchmark smoke.
104
+ - **Deferred (deliberately):** decorating Fastify's own `request`/`reply` for pure-JSON parity —
105
+ it would override Fastify's reply API (`send`/`type`/`header`/`status`/`code`/`getHeader`) with
106
+ Express semantics and break Fastify internals. Correctness ("nothing breaks") outranks that
107
+ win; the safe separate-`res` design stays. Tracked in `docs/PLAN_FASTIFY_CLOSER.md`.
108
+
109
+ ## Correctness (release blocker fixed)
110
+
111
+ - **Per-request state isolation in the Fastify lane.** The previous adapter reused ONE `req`
112
+ and ONE `res` object app-wide (`createRequestAdapter`/`createResponseAdapter`). That only
113
+ held for synchronous handlers; under concurrent **async** handlers (or morgan's deferred
114
+ finish listener) later requests overwrote earlier ones. Repro: 29/30 concurrent async
115
+ `GET /echo/:id` returned empty/scrambled bodies. Fixed with a shared method prototype + a
116
+ small per-request instance (`Object.create`). `src/fastify/adapters/{request,response}.ts`.
117
+ - **Morgan logs now correct under concurrency.** Lifecycle events (finish/end/close) are
118
+ delegated to the real `reply.raw` response, so morgan/on-finished fire on the actual
119
+ response finish (status, headers, `res._startAt` all set). Removed the synthetic
120
+ double-`setImmediate` finish emit that raced under load and produced "- - ms - -".
121
+ - New regression tests: `test/integration/concurrency.test.js` (async echo, distinct
122
+ status/headers/body, res.locals isolation, morgan under concurrent load) and
123
+ `test/integration/middleware-parity.test.js` (auth `req.user` isolation, helmet,
124
+ express.json, cookies). Wired into `npm test`.
125
+
126
+ ## Performance (Fastify lane)
127
+
128
+ - compile.ts: one `next()` per chain (not per handler); `baseUrl` precomputed at registration;
129
+ zero-middleware routes folded into a single Fastify handler (no preHandler stage); handler
130
+ returns the user handler's promise directly (no extra async frame). `res.locals` is lazy.
131
+ - Removed the per-request `reply.raw.writeHead` morgan patch; `res._startAt` is recorded in
132
+ `res.json/send` instead.
133
+ - `benchmarks/fast-vs-fastify/run.js`: warmup + interleaved median over N rounds (env `ROUNDS`,
134
+ `WARMUP`) for trustworthy numbers.
135
+ - Baseline after fixes: fast() ≈ 0.72× Fastify (MW=0 and MW=5), stable; fast() **beats** plain
136
+ Express on most scenarios and **beats** Fastify on middleware-stack. CPU profile shows ~84%
137
+ of time in Node's C++ HTTP layer and our own JS <1% — remaining gap is per-request Express
138
+ req/res allocation over Fastify. Auth gap vs Fastify is the JWT library (jsonwebtoken vs
139
+ @fastify/jwt), not the adapter (fast() ≈ Express there).
140
+
141
+ ## Next step (documented, not yet done)
142
+
143
+ - Path to ~parity: decorate Fastify's own `request`/`reply` (`decorateRequest`/`decorateReply`)
144
+ so no extra per-request objects are allocated. See `docs/PLAN_FASTIFY_CLOSER.md` status note.
145
+
146
+ ## Known limitation — RESOLVED
147
+
148
+ - The previously-noted "detached `next()` skips subsequent middleware" limitation is **fixed** by
149
+ the continuation-based middleware chain (see above). All middleware styles now behave as in
150
+ Express. The only thing that hangs is a handler that never calls `next()` and never responds —
151
+ which hangs in real Express too.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Express-like app stub: app.use, app.get, app.listen.
3
+ * Routes are immutable after listen(). Full Router handling is in lifecycle.ts.
4
+ */
5
+ import type { ExpressLikeApp as IExpressLikeApp } from '../types/internal';
6
+ export declare function createExpressLikeApp(routeStore: import('./RouteStore').RouteStore, locked: {
7
+ current: boolean;
8
+ }): IExpressLikeApp;
9
+ //# sourceMappingURL=ExpressLikeApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpressLikeApp.d.ts","sourceRoot":"","sources":["../../src/app/ExpressLikeApp.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,cAAc,IAAI,eAAe,EAA0B,MAAM,mBAAmB,CAAC;AAGnG,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,OAAO,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,mBAqE/G"}
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * Express-like app stub: app.use, app.get, app.listen.
4
+ * Routes are immutable after listen(). Full Router handling is in lifecycle.ts.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createExpressLikeApp = createExpressLikeApp;
8
+ const assert_1 = require("../utils/assert");
9
+ function createExpressLikeApp(routeStore, locked) {
10
+ const app = {
11
+ use(pathOrHandler, ...handlers) {
12
+ (0, assert_1.assertNotLocked)(locked.current);
13
+ const path = typeof pathOrHandler === 'string' ? pathOrHandler : '/';
14
+ const allHandlers = typeof pathOrHandler === 'string' ? handlers : [pathOrHandler, ...handlers];
15
+ routeStore.addMiddleware(path, ...allHandlers);
16
+ return app;
17
+ },
18
+ get(path, ...handlers) {
19
+ (0, assert_1.assertNotLocked)(locked.current);
20
+ routeStore.addRoute('get', path, ...handlers);
21
+ return app;
22
+ },
23
+ post(path, ...handlers) {
24
+ (0, assert_1.assertNotLocked)(locked.current);
25
+ routeStore.addRoute('post', path, ...handlers);
26
+ return app;
27
+ },
28
+ put(path, ...handlers) {
29
+ (0, assert_1.assertNotLocked)(locked.current);
30
+ routeStore.addRoute('put', path, ...handlers);
31
+ return app;
32
+ },
33
+ patch(path, ...handlers) {
34
+ (0, assert_1.assertNotLocked)(locked.current);
35
+ routeStore.addRoute('patch', path, ...handlers);
36
+ return app;
37
+ },
38
+ delete(path, ...handlers) {
39
+ (0, assert_1.assertNotLocked)(locked.current);
40
+ routeStore.addRoute('delete', path, ...handlers);
41
+ return app;
42
+ },
43
+ head(path, ...handlers) {
44
+ (0, assert_1.assertNotLocked)(locked.current);
45
+ routeStore.addRoute('head', path, ...handlers);
46
+ return app;
47
+ },
48
+ options(path, ...handlers) {
49
+ (0, assert_1.assertNotLocked)(locked.current);
50
+ routeStore.addRoute('options', path, ...handlers);
51
+ return app;
52
+ },
53
+ all(path, ...handlers) {
54
+ (0, assert_1.assertNotLocked)(locked.current);
55
+ routeStore.addRoute('all', path, ...handlers);
56
+ return app;
57
+ },
58
+ listen(_port, _host, _callback) {
59
+ throw new Error('Use createApp() from the main entry; listen() is implemented there.');
60
+ },
61
+ };
62
+ return app;
63
+ }
64
+ //# sourceMappingURL=ExpressLikeApp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpressLikeApp.js","sourceRoot":"","sources":["../../src/app/ExpressLikeApp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Stores routes and middleware before compile. Immutable after listen().
3
+ */
4
+ import type { RouteEntry } from '../types/internal';
5
+ import type { ExpressHandler } from '../types/express';
6
+ export declare class RouteStore {
7
+ private entries;
8
+ addMiddleware(path: string, ...handlers: ExpressHandler[]): void;
9
+ addRoute(method: string, path: string, ...handlers: ExpressHandler[]): void;
10
+ /** Add a single entry (used when flattening Router). */
11
+ addEntry(entry: RouteEntry): void;
12
+ /** Add multiple entries (used when flattening Router). */
13
+ addEntries(entries: readonly RouteEntry[]): void;
14
+ getAll(): readonly RouteEntry[];
15
+ clear(): void;
16
+ }
17
+ //# sourceMappingURL=RouteStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouteStore.d.ts","sourceRoot":"","sources":["../../src/app/RouteStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAoB;IAEnC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,cAAc,EAAE,GAAG,IAAI;IAIhE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,cAAc,EAAE,GAAG,IAAI;IAQ3E,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAQjC,0DAA0D;IAC1D,UAAU,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI;IAMhD,MAAM,IAAI,SAAS,UAAU,EAAE;IAI/B,KAAK,IAAI,IAAI;CAGd"}