exisjs 0.7.11 → 0.7.13

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 (150) hide show
  1. package/README.md +195 -39
  2. package/dist/cli/commands/dev.d.ts +4 -0
  3. package/dist/cli/commands/dev.d.ts.map +1 -1
  4. package/dist/cli/commands/dev.js +136 -32
  5. package/dist/cli/commands/dev.js.map +1 -1
  6. package/dist/cli/commands/run.d.ts +7 -0
  7. package/dist/cli/commands/run.d.ts.map +1 -0
  8. package/dist/cli/commands/run.js +99 -0
  9. package/dist/cli/commands/run.js.map +1 -0
  10. package/dist/cli/index.js +29 -13
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/manifest.d.ts.map +1 -1
  13. package/dist/cli/manifest.js +18 -3
  14. package/dist/cli/manifest.js.map +1 -1
  15. package/dist/cli/routes.d.ts +6 -0
  16. package/dist/cli/routes.d.ts.map +1 -1
  17. package/dist/cli/routes.js +224 -135
  18. package/dist/cli/routes.js.map +1 -1
  19. package/dist/config/config.d.ts +2 -1
  20. package/dist/config/config.d.ts.map +1 -1
  21. package/dist/config/config.js.map +1 -1
  22. package/dist/config/types.d.ts +30 -9
  23. package/dist/config/types.d.ts.map +1 -1
  24. package/dist/database/lifecycle.d.ts +6 -1
  25. package/dist/database/lifecycle.d.ts.map +1 -1
  26. package/dist/database/lifecycle.js +15 -4
  27. package/dist/database/lifecycle.js.map +1 -1
  28. package/dist/decorators/params.d.ts +21 -0
  29. package/dist/decorators/params.d.ts.map +1 -1
  30. package/dist/decorators/params.js +21 -1
  31. package/dist/decorators/params.js.map +1 -1
  32. package/dist/di/container.d.ts +15 -6
  33. package/dist/di/container.d.ts.map +1 -1
  34. package/dist/di/container.js +26 -3
  35. package/dist/di/container.js.map +1 -1
  36. package/dist/di/index.d.ts +1 -1
  37. package/dist/di/index.d.ts.map +1 -1
  38. package/dist/di/index.js.map +1 -1
  39. package/dist/di/inject.d.ts +2 -1
  40. package/dist/di/inject.d.ts.map +1 -1
  41. package/dist/di/inject.js +2 -1
  42. package/dist/di/inject.js.map +1 -1
  43. package/dist/docs/BENCHMARKS.md +4 -4
  44. package/dist/docs/content/README.md +43 -44
  45. package/dist/docs/content/boundaries.mdx +141 -48
  46. package/dist/docs/content/cli/commands.mdx +161 -107
  47. package/dist/docs/content/cli/overview.mdx +61 -46
  48. package/dist/docs/content/cli/usage.mdx +107 -30
  49. package/dist/docs/content/config.mdx +145 -147
  50. package/dist/docs/content/controllers.mdx +193 -464
  51. package/dist/docs/content/cron.mdx +183 -69
  52. package/dist/docs/content/decorators.mdx +449 -154
  53. package/dist/docs/content/errors.mdx +202 -305
  54. package/dist/docs/content/exisjs-mcp.mdx +59 -49
  55. package/dist/docs/content/installation.mdx +243 -157
  56. package/dist/docs/content/introduction.mdx +220 -48
  57. package/dist/docs/content/middleware.mdx +171 -497
  58. package/dist/docs/content/modules.mdx +141 -83
  59. package/dist/docs/content/providers.mdx +216 -180
  60. package/dist/docs/content/requests.mdx +168 -488
  61. package/dist/docs/content/responses.mdx +200 -533
  62. package/dist/docs/content/routing.mdx +239 -558
  63. package/dist/docs/content/security.mdx +233 -57
  64. package/dist/docs/content/structure.mdx +151 -240
  65. package/dist/docs/content/swagger.mdx +192 -91
  66. package/dist/docs/content/testing.mdx +205 -74
  67. package/dist/docs/content/validation.mdx +479 -341
  68. package/dist/docs/reference/README.md +34 -2
  69. package/dist/docs/reference/config.mdx +277 -73
  70. package/dist/docs/reference/context.mdx +197 -106
  71. package/dist/docs/reference/cron.mdx +266 -152
  72. package/dist/docs/reference/database.mdx +135 -53
  73. package/dist/docs/reference/decorators.mdx +234 -116
  74. package/dist/docs/reference/di.mdx +146 -112
  75. package/dist/docs/reference/error.mdx +155 -178
  76. package/dist/docs/reference/exis.mdx +141 -70
  77. package/dist/docs/reference/logger.mdx +110 -126
  78. package/dist/docs/reference/middleware.mdx +235 -158
  79. package/dist/docs/reference/plugin.mdx +136 -64
  80. package/dist/docs/reference/router.mdx +277 -251
  81. package/dist/docs/reference/sanitize.mdx +91 -87
  82. package/dist/docs/reference/swagger.mdx +280 -100
  83. package/dist/docs/reference/testing.mdx +223 -89
  84. package/dist/docs/reference/validator.mdx +194 -206
  85. package/dist/lib/inspect-routes.d.ts +2 -0
  86. package/dist/lib/inspect-routes.d.ts.map +1 -0
  87. package/dist/lib/inspect-routes.js +148 -0
  88. package/dist/lib/inspect-routes.js.map +1 -0
  89. package/dist/lib/start-server.js +13 -8
  90. package/dist/lib/start-server.js.map +1 -1
  91. package/dist/module/module.d.ts +6 -3
  92. package/dist/module/module.d.ts.map +1 -1
  93. package/dist/module/module.js +17 -3
  94. package/dist/module/module.js.map +1 -1
  95. package/dist/router/controller-registrar.d.ts.map +1 -1
  96. package/dist/router/controller-registrar.js +26 -4
  97. package/dist/router/controller-registrar.js.map +1 -1
  98. package/dist/router/scanner/cron-mounter.d.ts.map +1 -1
  99. package/dist/router/scanner/cron-mounter.js +9 -2
  100. package/dist/router/scanner/cron-mounter.js.map +1 -1
  101. package/dist/server/app.d.ts +3 -1
  102. package/dist/server/app.d.ts.map +1 -1
  103. package/dist/server/app.js +29 -8
  104. package/dist/server/app.js.map +1 -1
  105. package/dist/server/define.d.ts.map +1 -1
  106. package/dist/server/define.js +5 -0
  107. package/dist/server/define.js.map +1 -1
  108. package/dist/server/request.d.ts +2 -0
  109. package/dist/server/request.d.ts.map +1 -1
  110. package/dist/server/request.js +11 -0
  111. package/dist/server/request.js.map +1 -1
  112. package/dist/swagger/generator.d.ts.map +1 -1
  113. package/dist/swagger/generator.js +130 -10
  114. package/dist/swagger/generator.js.map +1 -1
  115. package/dist/telemetry/index.d.ts +12 -0
  116. package/dist/telemetry/index.d.ts.map +1 -0
  117. package/dist/telemetry/index.js +121 -0
  118. package/dist/telemetry/index.js.map +1 -0
  119. package/dist/testing/client.d.ts +1 -0
  120. package/dist/testing/client.d.ts.map +1 -1
  121. package/dist/testing/client.js +4 -1
  122. package/dist/testing/client.js.map +1 -1
  123. package/dist/testing/index.d.ts.map +1 -1
  124. package/dist/testing/index.js +20 -4
  125. package/dist/testing/index.js.map +1 -1
  126. package/dist/validator/builder.d.ts +26 -1
  127. package/dist/validator/builder.d.ts.map +1 -1
  128. package/dist/validator/builder.js +31 -0
  129. package/dist/validator/builder.js.map +1 -1
  130. package/dist/validator/engine.d.ts +23 -0
  131. package/dist/validator/engine.d.ts.map +1 -1
  132. package/dist/validator/engine.js +228 -31
  133. package/dist/validator/engine.js.map +1 -1
  134. package/dist/validator/index.d.ts +8 -4
  135. package/dist/validator/index.d.ts.map +1 -1
  136. package/dist/validator/index.js +4 -1
  137. package/dist/validator/index.js.map +1 -1
  138. package/dist/validator/tex-types.d.ts +15 -0
  139. package/dist/validator/tex-types.d.ts.map +1 -1
  140. package/dist/validator/tex-types.js +102 -0
  141. package/dist/validator/tex-types.js.map +1 -1
  142. package/dist/validator/tex.d.ts +1 -1
  143. package/dist/validator/tex.d.ts.map +1 -1
  144. package/dist/validator/tex.js +3 -1
  145. package/dist/validator/tex.js.map +1 -1
  146. package/dist/validator/types.d.ts +1 -0
  147. package/dist/validator/types.d.ts.map +1 -1
  148. package/package.json +47 -14
  149. package/dist/docs/content/dataloader.mdx +0 -170
  150. package/dist/docs/reference/dataloader.mdx +0 -91
@@ -1,522 +1,244 @@
1
1
  ---
2
- nextUrl: "/docs/modules"
3
- nextTitle: "Modules & Plugins"
2
+ title: "Middleware Pipeline"
3
+ description: "Configure global, folder-scoped, and route-level middlewares, type-safe context injection, file uploads, and Express bridges in ExisJS."
4
+ nextUrl: "/docs/errors"
5
+ nextTitle: "Error Handling"
4
6
  ---
5
7
 
6
- # Middleware
8
+ # Middleware Pipeline
7
9
 
8
- ExisJS comes with a collection of built-in middleware out of the box.
10
+ ExisJS provides a unified middleware pipeline supporting three execution levels:
11
+ 1. **Global Middlewares (`src/http/server.ts` or `exis.config.ts`)**: Apply to every incoming HTTP request across the server.
12
+ 2. **Folder-Scoped Middlewares (`src/http/**/boundary.ts`)**: Apply to all routes within a directory and its subdirectories.
13
+ 3. **Route-Scoped Middlewares (`route.ts`)**: Apply to specific individual endpoints.
9
14
 
10
- ExisJS provides first-party implementations for features like CORS, Request Logging, Backpressure, In-Memory Rate Limiting, Idempotency, and Deduplication.
11
-
12
- All middleware can be imported directly from the `exisjs/middleware` package.
15
+ All standard middlewares can be imported directly from `exisjs/middleware`. Custom typed middlewares can be declared using `defineMiddleware<TContext>()` from `exisjs/router`.
13
16
 
14
17
  ---
15
18
 
16
- ## Where to Apply Middleware
17
-
18
- ExisJS gives you granular control over where your middleware executes:
19
-
20
- 1. **Globally (`server.ts`)**: Applies to every request hitting your server via `app.use()`.
21
- 2. **Folder-Scoped (`boundary.ts`)**: Applies to routes inside that folder and all subfolders automatically.
22
- 3. **Route-Scoped (`route.ts`)**: Applies only to a specific endpoint.
23
-
24
- ```typescript filename="src/http/admin/boundary.ts"
25
- import { defineBoundary } from 'exisjs/router'
26
- import type { Request, Response, Next } from 'exisjs/router'
27
- import { requestLogger } from 'exisjs/middleware'
28
-
29
- export const config = defineBoundary({})
30
-
31
- // Auto-detected chain step for /admin/*
32
- export const logger = requestLogger({ level: 'debug' })
33
- ```
34
-
35
- ### Middleware Context Type Inference (`defineMiddleware`)
36
-
37
- When custom middlewares attach data to `req` (such as authenticated user details, session data, or tenant tokens), ExisJS supports **End-to-End Middleware Context Type Inference**.
38
-
39
- By using `defineMiddleware<TContext>()`, downstream route handlers automatically inherit strongly typed properties on `req` with zero manual type casts (`as any`) or boilerplate.
40
-
41
- #### Local Route-Level Inference (Recommended)
42
-
43
- ```typescript filename="src/http/route.ts"
44
- import { controller, route, defineMiddleware } from 'exisjs/router'
45
-
46
- // 1. Define your custom context shape
47
- interface AuthContext {
48
- user: {
49
- id: string
50
- role: 'admin' | 'user'
51
- email: string
52
- }
53
- }
54
-
55
- // 2. Define the middleware with your context
56
- export const authMiddleware = defineMiddleware<AuthContext>((req, res, next) => {
57
- req.user = {
58
- id: 'usr_123',
59
- role: 'admin',
60
- email: 'alex@example.com'
61
- }
62
- next()
63
- })
64
-
65
- export default controller({
66
- profile: route.get('/profile', {
67
- middlewares: [authMiddleware],
68
- handle({ req, res }) {
69
- // ✨ req.user is automatically inferred directly from middlewares: [authMiddleware]!
70
- // Full autocomplete and strict type safety:
71
- return res.json({
72
- userId: req.user.id,
73
- role: req.user.role,
74
- email: req.user.email
75
- })
76
- }
77
- })
78
- })
79
- ```
80
-
81
- #### Multi-Middleware Context Composition
82
-
83
- When passing multiple middlewares, ExisJS automatically merges all injected contexts:
84
-
85
- ```typescript
86
- const authMw = defineMiddleware<{ user: { id: string } }>(...)
87
- const tenantMw = defineMiddleware<{ tenantId: string }>(...)
19
+ ## Built-in Middlewares Reference
20
+
21
+ | Middleware | Signature | Description |
22
+ | :--- | :--- | :--- |
23
+ | `cors` | `cors(options?)` | Cross-Origin Resource Sharing handler for allowed origins, methods, and preflight headers. |
24
+ | `requestLogger` | `requestLogger(options?)` | Request logger powered by Pino, injecting `req.log` with automatic timing and child loggers. |
25
+ | `requestId` | `requestId()` | Attaches a unique `X-Request-Id` to requests and responses with distributed tracing headers. |
26
+ | `compress` | `compress()` | Stream-based HTTP response compression supporting Gzip, Brotli, and Deflate. |
27
+ | `rateLimit` | `rateLimit(options?)` | Off-heap / in-memory rate limiter returning `429 Too Many Requests` with standard headers. |
28
+ | `idempotent` | `idempotent(options?)` | Duplicate mutation caching keyed by `Idempotency-Key` headers. |
29
+ | `dedupe` | `dedupe(options)` | Merges simultaneous identical in-flight `GET` requests into a single operation. |
30
+ | `backpressure` | `backpressure(options?)` | Queues requests during high load and sheds excess traffic with `503 Service Unavailable`. |
31
+ | `serveStatic` | `serveStatic(root, options?)` | Serves static assets from disk with strong ETags and Brotli/Gzip pre-compression. |
32
+ | `timeout` | `timeout(ms \| options)` | Terminates requests exceeding the duration limit with `503 Service Unavailable`. |
33
+ | `fileUpload` | `fileUpload(options?)` | Streaming multipart/form-data parser supporting disk/memory storage and MIME checks. |
34
+ | `fromExpress` | `fromExpress(middleware)` | Connect/Express bridge adapter running third-party middleware (e.g. Multer) directly. |
88
35
 
89
- export default controller({
90
- data: route.get('/data', {
91
- middlewares: [authMw, tenantMw],
92
- handle({ req }) {
93
- // req has both `req.user` and `req.tenantId` strictly typed!
94
- return { userId: req.user.id, tenant: req.tenantId }
95
- }
96
- })
97
- })
98
- ```
99
-
100
- #### Module-Scoped Context (`createRouter<TContext>()`)
101
-
102
- If an entire file or subsystem shares the same context, use `createRouter<TContext>()`:
36
+ ---
103
37
 
104
- ```typescript filename="src/http/admin/route.ts"
105
- import { createRouter } from 'exisjs/router'
38
+ ## Where to Apply Middleware
106
39
 
107
- interface AdminContext {
108
- user: { id: string; role: 'admin' }
109
- }
40
+ ### 1. Global Middleware (`src/http/server.ts`)
110
41
 
111
- const { route, controller } = createRouter<AdminContext>()
42
+ ```typescript filename="src/http/server.ts"
43
+ import { exis } from 'exisjs'
44
+ import { requestId, requestLogger, compress } from 'exisjs/middleware'
112
45
 
113
- export default controller({
114
- settings: route.get('/settings', {
115
- middlewares: [authMiddleware],
116
- handle({ req }) {
117
- return { adminId: req.user.id }
118
- }
119
- })
46
+ export default exis({
47
+ async onStart(app) {
48
+ app.use(requestId())
49
+ app.use(requestLogger({ level: 'info' }))
50
+ app.use(compress())
51
+ },
120
52
  })
121
53
  ```
122
54
 
123
- #### Boundary-Scoped Context (`defineBoundary<TContext>()`)
55
+ ### 2. Folder-Scoped Middleware (`src/http/**/boundary.ts`)
124
56
 
125
- Propagate context types across all routes in a directory tree:
57
+ Middlewares defined in a `boundary.ts` apply to all routes in that folder and child folders:
126
58
 
127
59
  ```typescript filename="src/http/admin/boundary.ts"
128
60
  import { defineBoundary } from 'exisjs/router'
129
-
130
- export const config = defineBoundary<AdminContext>({
131
- beforeHandle: async ({ req }) => {
132
- console.log(`Admin action by: ${req.user.id}`)
133
- }
134
- })
135
- ```
136
-
137
- #### Class-Based (OOP) Context Usage
138
-
139
- In OOP controllers, you can attach typed middlewares via `@Use()` and type the injected `@Req()` object:
140
-
141
- ```typescript filename="src/http/admin/route.ts"
142
- import { Controller, Get, Use, Req } from 'exisjs/decorators'
143
- import type { Request } from 'exisjs/router'
144
- import { authMiddleware } from '../../middleware/auth'
145
- import type { AuthContext } from '../../middleware/auth'
146
-
147
- @Controller('/admin')
148
- @Use(authMiddleware)
149
- export default class AdminController {
150
- @Get('/profile')
151
- getProfile(@Req() req: Request & AuthContext) {
152
- return {
153
- userId: req.user.id,
154
- role: req.user.role,
155
- email: req.user.email,
156
- }
157
- }
158
- }
159
- ```
160
-
161
- #### Functional Paradigm
162
-
163
- ```typescript filename="src/http/route.ts"
164
- import { controller, route } from 'exisjs/router'
165
- import { validate } from 'exisjs/middleware'
166
- import { tex } from 'exisjs/validator'
167
-
168
- const mySchema = {
169
- body: tex.object({ name: tex.string() })
170
- }
171
-
172
- export default controller({
173
- create: route.post('/', {
174
- middlewares: [validate(mySchema)],
175
- handle({ req }) {
176
- return { ok: true, name: req.body.name }
177
- }
178
- })
179
- })
180
- ```
181
-
182
- #### Class-Based (OOP) Paradigm
183
-
184
- ```typescript filename="src/http/route.ts"
185
- import { Controller, Post, Use } from 'exisjs/decorators'
186
- import { validate } from 'exisjs/middleware'
187
- import { tex } from 'exisjs/validator'
188
-
189
- const mySchema = {
190
- body: tex.object({ name: tex.string() })
191
- }
192
-
193
- @Controller()
194
- export default class MyController {
195
- @Use(validate(mySchema))
196
- @Post('/')
197
- async create() {
198
- return { ok: true }
199
- }
200
- }
201
- ```
202
-
203
- ---
204
-
205
- ## 1. CORS
206
-
207
- The `cors` middleware handles Cross-Origin Resource Sharing.
208
-
209
- The recommended practice in ExisJS is to configure CORS globally in your **`exis.config.ts`** file or per-folder in `boundary.ts`.
210
-
211
- ```typescript filename="exis.config.ts"
212
- import { defineConfig } from 'exisjs/config'
213
-
214
- export default async () => defineConfig({
215
- cors: {
216
- origin: ['https://myapp.com', /localhost:\d+/],
217
- methods: ['GET', 'POST', 'OPTIONS'],
218
- credentials: true,
219
- maxAge: 86400,
220
- }
221
- })
222
- ```
223
-
224
- ---
225
-
226
- ## 2. Request Logger
227
-
228
- ExisJS uses Pino under the hood. The `requestLogger` injects a request-scoped logger instance (`req.log`) and times response cycles automatically.
229
-
230
- ```typescript filename="src/http/server.ts"
231
- import { requestLogger } from 'exisjs/middleware'
232
-
233
- app.use(requestLogger({
234
- level: 'debug',
235
- pretty: true
236
- }))
237
- ```
238
-
239
- ---
240
-
241
- ## 3. Request ID
242
-
243
- Injects a unique `X-Request-Id` into every incoming request and outgoing response header. Supports distributed tracing headers (`traceparent`, `x-b3-traceid`).
244
-
245
- ```typescript filename="src/http/server.ts"
246
- import { requestId } from 'exisjs/middleware'
247
-
248
- app.use(requestId())
249
- ```
250
-
251
- ---
252
-
253
- ## 4. Rate Limiting (In-Memory / Native Off-Heap)
254
-
255
- ExisJS provides high-performance rate limiting powered by Rust native memory:
256
-
257
- #### Functional Paradigm
258
-
259
- ```typescript filename="src/http/auth/route.ts"
260
- import { controller, route } from 'exisjs/router'
261
61
  import { rateLimit } from 'exisjs/middleware'
62
+ import { authGuard } from '../../common/guards/auth.guard'
262
63
 
263
- const authLimiter = rateLimit({ windowMs: 60000, max: 10 })
264
-
265
- export default controller({
266
- login: route.post('/login', {
267
- middlewares: [authLimiter],
268
- handle({ req }) {
269
- return { status: 'ok' }
270
- }
271
- })
64
+ const adminLimiter = rateLimit({
65
+ windowMs: 60 * 1000,
66
+ max: 60,
67
+ message: 'Admin rate limit exceeded',
272
68
  })
273
- ```
274
-
275
- #### Class-Based (OOP) Paradigm
276
-
277
- ```typescript filename="src/http/auth/route.ts"
278
- import { Controller, Post, Use } from 'exisjs/decorators'
279
- import { rateLimit } from 'exisjs/middleware'
280
-
281
- const authLimiter = rateLimit({ windowMs: 60000, max: 10 })
282
-
283
- @Controller('/auth')
284
- export default class AuthController {
285
- @Post('/login')
286
- @Use(authLimiter)
287
- async login() {
288
- return { status: 'ok' }
289
- }
290
- }
291
- ```
292
-
293
- > [!NOTE]
294
- > Core rate limiting runs completely off-heap in-memory. For Redis-backed distributed rate limiting across clusters, use `@exisjs/cache`.
295
-
296
- ---
297
-
298
- ## 5. Idempotency (In-Memory / Native Off-Heap)
299
-
300
- Idempotency caches responses based on the `Idempotency-Key` header, preventing duplicate mutations:
301
-
302
- #### Functional Paradigm
303
69
 
304
- ```typescript filename="src/http/checkout/route.ts"
305
- import { controller, route } from 'exisjs/router'
306
- import { idempotent } from 'exisjs/middleware'
307
-
308
- export default controller({
309
- checkout: route.post('/', {
310
- middlewares: [idempotent({ ttlMs: 86400000 })],
311
- async handle() {
312
- return { success: true }
313
- }
314
- })
70
+ export default defineBoundary({
71
+ middleware: [adminLimiter, authGuard],
315
72
  })
316
73
  ```
317
74
 
318
- #### Class-Based (OOP) Paradigm
319
-
320
- ```typescript filename="src/http/checkout/route.ts"
321
- import { Controller, Post, Idempotent } from 'exisjs/decorators'
322
-
323
- @Controller('/checkout')
324
- export default class CheckoutController {
325
- @Idempotent({ ttlMs: 86400000 })
326
- @Post('/')
327
- async checkout() {
328
- return { success: true }
329
- }
330
- }
331
- ```
332
-
333
- ---
334
-
335
- ## 6. Request Deduplication (`dedupe`)
336
-
337
- If multiple clients request the exact same resource concurrently, `dedupe` collapses identical in-flight requests into a single operation:
75
+ ### 3. Route-Scoped Middleware (`src/http/**/route.ts`)
338
76
 
339
- #### Functional Paradigm
77
+ #### Functional Route Paradigm
340
78
 
341
- ```typescript filename="src/http/stats/route.ts"
79
+ ```typescript filename="src/http/reports/route.ts"
342
80
  import { controller, route } from 'exisjs/router'
343
81
  import { dedupe } from 'exisjs/middleware'
344
82
 
345
83
  export default controller({
346
- getStats: route.get('/', {
347
- middlewares: [dedupe({ ttlMs: 5000 })],
348
- async handle() {
349
- return { activeUsers: 1000 }
350
- }
351
- })
84
+ generate: route.get('/', {
85
+ middleware: [
86
+ dedupe({
87
+ keyGenerator: (req) => `${req.user?.id || req.ip}:reports`,
88
+ }),
89
+ ],
90
+ async handle({ res }) {
91
+ const data = await computeReport()
92
+ res.json({ data })
93
+ },
94
+ }),
352
95
  })
353
96
  ```
354
97
 
355
- #### Class-Based (OOP) Paradigm
98
+ #### Class-Based (OOP) Controller Paradigm
356
99
 
357
- ```typescript filename="src/http/stats/route.ts"
100
+ ```typescript filename="src/http/reports/controller.ts"
358
101
  import { Controller, Get, Use } from 'exisjs/decorators'
359
102
  import { dedupe } from 'exisjs/middleware'
360
103
 
361
- @Controller('/stats')
362
- export default class StatsController {
104
+ @Controller('/reports')
105
+ export default class ReportsController {
363
106
  @Get('/')
364
- @Use(dedupe({ ttlMs: 5000 }))
365
- async getStats() {
366
- return { activeUsers: 1000 }
107
+ @Use(
108
+ dedupe({
109
+ keyGenerator: (req) => `${req.user?.id || req.ip}:reports`,
110
+ })
111
+ )
112
+ async generate() {
113
+ return { data: await computeReport() }
367
114
  }
368
115
  }
369
116
  ```
370
117
 
371
118
  ---
372
119
 
373
- ## 7. Static File Serving
374
-
375
- A static file server that resolves MIME types and streams files:
376
-
377
- ```typescript
378
- import { serveStatic } from 'exisjs/middleware'
379
- import path from 'node:path'
380
-
381
- app.use('/public', serveStatic(path.join(process.cwd(), 'public'), {
382
- maxAge: 31536000
383
- }))
384
- ```
120
+ ## Type-Safe Middleware Context (`defineMiddleware`)
385
121
 
386
- ---
387
-
388
- ## 8. Compression (Gzip / Brotli / Deflate)
122
+ When custom middlewares augment `req` with extra properties (such as user sessions or tenant tokens), use `defineMiddleware<TContext>()` from `exisjs/router`. Downstream route handlers will automatically infer the injected properties on `req`:
389
123
 
390
- Stream-based compression middleware supporting Gzip, Deflate, and Brotli:
124
+ ### 1. Declaring a Typed Middleware
391
125
 
392
- ```typescript filename="exis.config.ts"
393
- import type { ExisConfig } from 'exisjs/config'
126
+ ```typescript filename="src/common/middleware/auth.ts"
127
+ import { defineMiddleware } from 'exisjs/router'
128
+ import { UnauthorizedException } from 'exisjs/error'
394
129
 
395
- const config: ExisConfig = {
396
- compression: true,
130
+ export interface AuthContext {
131
+ user: {
132
+ id: string
133
+ role: 'admin' | 'member'
134
+ email: string
135
+ }
397
136
  }
398
137
 
399
- export default config
400
- ```
401
-
402
- ---
403
-
404
- ## 9. Request Timeout (`timeout`)
405
-
406
- The `timeout()` middleware terminates runaway requests and returns `503 Service Unavailable` (or a custom status code).
407
-
408
- ExisJS defaults to **60 seconds (60000ms)** to provide generous headroom for slow network connections and third-party cloud operations (Cloudinary, AWS S3, OpenAI, Stripe).
409
-
410
- ```typescript filename="src/http/server.ts"
411
- import { exis } from 'exisjs'
412
- import { timeout } from 'exisjs/middleware'
138
+ export const requireAuth = defineMiddleware<AuthContext>(async (req, res, next) => {
139
+ const token = req.headers['authorization']
140
+ if (!token) {
141
+ throw new UnauthorizedException('Authentication token missing')
142
+ }
413
143
 
414
- export default exis({
415
- async onStart(app) {
416
- app.use(timeout({
417
- ms: 60000,
418
- statusCode: 504,
419
- message: 'Gateway Timeout connecting to upstream cloud service',
420
- // Bypass webhooks or long-lived streaming endpoints
421
- exclude: ['/webhooks/*', '/ai/stream'],
422
- }))
144
+ req.user = {
145
+ id: 'user_456',
146
+ role: 'admin',
147
+ email: 'dev@example.com',
423
148
  }
149
+
150
+ next()
424
151
  })
425
152
  ```
426
153
 
427
- Handlers can also dynamically adjust timeouts using `req.setTimeout(ms)` and `req.clearTimeout()`.
428
-
429
- ### Route-Level Timeout Overrides
154
+ ### 2. Using Typed Middleware in Functional Routes
430
155
 
431
- Long-running routes (file uploads, PDF exports, AI inferences, data migrations) can declare custom timeouts directly in the route definition, overriding any global application timeout:
432
-
433
- ```typescript filename="src/http/ai/route.ts"
156
+ ```typescript filename="src/http/profile/route.ts"
434
157
  import { controller, route } from 'exisjs/router'
158
+ import { requireAuth } from '../../common/middleware/auth'
435
159
 
436
160
  export default controller({
437
- heavyTask: route.post('/heavy-task', {
438
- timeoutMs: 120000, // 2 minutes for this endpoint only
439
- async handle({ body }) {
440
- return await generateLargeReport(body)
441
- }
161
+ me: route.get('/', {
162
+ middleware: [requireAuth],
163
+ handle({ req, res }) {
164
+ // req.user is strongly typed as AuthContext['user'] without manual casts
165
+ res.json({
166
+ id: req.user.id,
167
+ email: req.user.email,
168
+ role: req.user.role,
169
+ })
170
+ },
442
171
  }),
443
-
444
- exportStream: route.get('/export', {
445
- timeoutMs: 0, // 0 disables timeout completely for infinite SSE / streaming
446
- async handle({ res }) {
447
- return streamDataToClient(res)
448
- }
449
- })
450
172
  })
451
173
  ```
452
174
 
453
- #### Class-Based (OOP) Paradigm
454
-
455
- Use the `@Timeout(ms)` decorator on any controller method:
456
-
457
- ```typescript filename="src/http/ai/controller.ts"
458
- import { Controller, Post, Timeout } from 'exisjs/decorators'
459
-
460
- @Controller('/ai')
461
- export default class AiController {
462
- @Post('/inference')
463
- @Timeout(120000)
464
- async runInference() {
465
- return await computeModel()
466
- }
467
- }
468
- ```
469
-
470
175
  ---
471
176
 
472
- ### Declarative Route Uploads
177
+ ## Multipart File Uploads (`fileUpload`)
178
+
179
+ ExisJS includes a streaming multipart parser in `exisjs/middleware` for file uploads:
473
180
 
474
- You can configure file uploads directly within functional route declarations:
181
+ ### 1. Functional Route Upload Declaration
475
182
 
476
183
  ```typescript filename="src/http/media/route.ts"
477
184
  import { controller, route } from 'exisjs/router'
185
+ import { fileUpload } from 'exisjs/middleware'
478
186
 
479
187
  export default controller({
480
- upload: route.post('/photo', {
481
- upload: {
482
- field: 'file',
483
- maxSize: '10MB',
484
- allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
485
- destination: 'memory', // 'memory' | 'disk' | 'stream'
188
+ uploadAvatar: route.post('/avatar', {
189
+ middleware: [
190
+ fileUpload.single('avatar', {
191
+ maxSize: '5MB',
192
+ allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
193
+ destination: 'memory',
194
+ }),
195
+ ],
196
+ async handle({ req, res }) {
197
+ const file = req.file
198
+ if (!file) {
199
+ return res.status(400).json({ error: 'No file uploaded' })
200
+ }
201
+
202
+ return res.status(201).json({
203
+ filename: file.filename,
204
+ mimetype: file.mimetype,
205
+ size: file.size,
206
+ })
486
207
  },
487
- async handle({ file, fields, req }) {
488
- // file.buffer, file.mimetype, file.filename are immediately accessible
489
- return { success: true, filename: file.filename }
490
- }
491
- })
208
+ }),
492
209
  })
493
210
  ```
494
211
 
495
- ### Class-Based (OOP) Upload Decorators
496
-
497
- Use `@UseUpload` or `@Upload` to configure file parsing on controller actions:
212
+ ### 2. Class-Based (OOP) Upload Decorator
498
213
 
499
214
  ```typescript filename="src/http/media/controller.ts"
500
215
  import { Controller, Post, UseUpload, UploadedFile, Body } from 'exisjs/decorators'
216
+ import type { ExisFile } from 'exisjs/middleware'
501
217
 
502
218
  @Controller('/media')
503
219
  export default class MediaController {
504
- @Post('/photo')
220
+ @Post('/avatar')
505
221
  @UseUpload({
506
- field: 'file',
507
- maxSize: '10MB',
222
+ field: 'avatar',
223
+ maxSize: '5MB',
508
224
  allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
509
225
  destination: 'memory',
510
226
  })
511
- async uploadPhoto(@UploadedFile() file: any, @Body() fields: any) {
512
- return { filename: file.filename, size: file.size }
227
+ async uploadAvatar(@UploadedFile() file: ExisFile, @Body() body: Record<string, string>) {
228
+ return {
229
+ filename: file.filename,
230
+ size: file.size,
231
+ title: body.title,
232
+ }
513
233
  }
514
234
  }
515
235
  ```
516
236
 
517
- ### Express / Connect Multer Bridge (`fromExpress`)
237
+ ---
238
+
239
+ ## Express and Connect Middleware Bridge (`fromExpress`)
518
240
 
519
- Seamlessly run existing Express/Connect middleware such as **Multer** directly in ExisJS without manual Promise wrapping:
241
+ To run legacy Express or Connect middleware (such as `multer`, `passport`, or custom third-party packages) directly inside ExisJS, wrap them using `fromExpress`:
520
242
 
521
243
  ```typescript filename="src/http/legacy/route.ts"
522
244
  import { controller, route } from 'exisjs/router'
@@ -526,91 +248,43 @@ import multer from 'multer'
526
248
  const upload = multer({ storage: multer.memoryStorage() })
527
249
 
528
250
  export default controller({
529
- upload: route.post('/',
251
+ upload: route.post(
252
+ '/',
530
253
  fromExpress(upload.single('file')),
531
- ({ req }) => {
532
- const file = (req.raw as any).file || req.file
533
- return { success: true, originalname: file?.originalname }
254
+ ({ req, res }) => {
255
+ const file = req.file
256
+ res.json({ success: true, originalname: file?.filename })
534
257
  }
535
- )
258
+ ),
536
259
  })
537
260
  ```
538
261
 
539
- ---
540
-
541
- ## 10. Security Scanner Noise Suppression & Blackhole (`blockSuspiciousProbes`)
542
-
543
- In public production deployments, automated vulnerability scanners aggressively probe for `.env`, `/.git/HEAD`, `/.env.backup`, `/.DS_Store`, `wp-config.php`, etc.
262
+ `fromExpress` automatically maps parameters, query strings, headers, and attached properties (like `req.file` or `req.body`) back onto the `ExisRequest` instance.
544
263
 
545
- Each probe generates a `404` error log with structured JSON logging (`{"level":40,"statusCode":404}`), flooding log aggregators and triggering false security alerts.
264
+ ---
546
265
 
547
- ExisJS provides `blockSuspiciousProbes` (alias `blockProbes`) to immediately intercept known exploit patterns, terminate the request with an empty response (blackhole), and suppress logger noise.
266
+ ## Static File Serving (`serveStatic`)
548
267
 
549
- ### Global Usage (`server.ts` / `exis.config.ts`)
268
+ Serve static frontend bundles, assets, or downloads directly from disk:
550
269
 
551
270
  ```typescript filename="src/http/server.ts"
552
271
  import { exis } from 'exisjs'
553
- import { blockSuspiciousProbes } from 'exisjs/middleware'
272
+ import { serveStatic } from 'exisjs/middleware'
273
+ import path from 'node:path'
554
274
 
555
275
  export default exis({
556
276
  async onStart(app) {
557
- // Intercept probes before downstream routes or loggers
558
- app.use(blockSuspiciousProbes({
559
- statusCode: 404, // or 403
560
- silent: true, // Suppresses level-40 warning logs in requestLogger
561
- }))
562
- }
563
- })
564
- ```
565
-
566
- Or enable directly in `exis.config.ts`:
567
-
568
- ```typescript filename="exis.config.ts"
569
- import { defineConfig } from 'exisjs/config'
570
-
571
- export default async () => defineConfig({
572
- blockProbes: true, // or { statusCode: 404, silent: true }
573
- })
574
- ```
575
-
576
- ### Folder-Scoped Boundary (`boundary.ts`)
577
-
578
- ```typescript filename="src/http/boundary.ts"
579
- import { defineBoundary } from 'exisjs/router'
580
-
581
- export default defineBoundary({
582
- blockProbes: {
583
- statusCode: 404,
584
- silent: true,
585
- exclude: ['/.well-known/security.txt'],
586
- }
277
+ app.use(
278
+ '/public',
279
+ serveStatic(path.join(process.cwd(), 'public'), {
280
+ maxAge: 31536000, // 1 year cache-control
281
+ etag: true,
282
+ brotli: true,
283
+ gzip: true,
284
+ })
285
+ )
286
+ },
587
287
  })
588
288
  ```
589
289
 
590
- ### Class-Based (OOP) Decorator
591
-
592
- ```typescript filename="src/http/api/route.ts"
593
- import { Controller, Get, BlockProbes } from 'exisjs/decorators'
594
-
595
- @Controller('/api')
596
- @BlockProbes({ statusCode: 404 })
597
- export default class ApiController {
598
- @Get('/health')
599
- health() {
600
- return { status: 'healthy' }
601
- }
602
- }
603
- ```
604
-
605
- ### Configuration Options
606
-
607
- | Option | Type | Default | Description |
608
- | :--- | :--- | :--- | :--- |
609
- | `statusCode` | `number` | `404` | HTTP status code to return for blocked probes |
610
- | `blackhole` | `boolean` | `true` | Immediately terminates connection with empty body without calling downstream middleware |
611
- | `silent` | `boolean` | `true` | Suppresses `requestLogger` warning logs for blocked probe requests |
612
- | `patterns` | `(string \| RegExp)[]` | `[]` | Additional custom probe patterns to block |
613
- | `exclude` | `(string \| RegExp)[]` | `[]` | Whitelist patterns to allow through without blocking |
614
- | `onProbe` | `(req, res, pattern) => void` | `undefined` | Callback hook when a probe is detected (for SIEM or IP reputation metrics) |
615
-
616
-
290
+ `serveStatic` automatically handles MIME detection, conditional `If-None-Match` 304 caching, and serves pre-compressed `.br` or `.gz` files if present on disk.