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.
- package/README.md +195 -39
- package/dist/cli/commands/dev.d.ts +4 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +136 -32
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/run.d.ts +7 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +99 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/index.js +29 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +18 -3
- package/dist/cli/manifest.js.map +1 -1
- package/dist/cli/routes.d.ts +6 -0
- package/dist/cli/routes.d.ts.map +1 -1
- package/dist/cli/routes.js +224 -135
- package/dist/cli/routes.js.map +1 -1
- package/dist/config/config.d.ts +2 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +30 -9
- package/dist/config/types.d.ts.map +1 -1
- package/dist/database/lifecycle.d.ts +6 -1
- package/dist/database/lifecycle.d.ts.map +1 -1
- package/dist/database/lifecycle.js +15 -4
- package/dist/database/lifecycle.js.map +1 -1
- package/dist/decorators/params.d.ts +21 -0
- package/dist/decorators/params.d.ts.map +1 -1
- package/dist/decorators/params.js +21 -1
- package/dist/decorators/params.js.map +1 -1
- package/dist/di/container.d.ts +15 -6
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +26 -3
- package/dist/di/container.js.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/index.js.map +1 -1
- package/dist/di/inject.d.ts +2 -1
- package/dist/di/inject.d.ts.map +1 -1
- package/dist/di/inject.js +2 -1
- package/dist/di/inject.js.map +1 -1
- package/dist/docs/BENCHMARKS.md +4 -4
- package/dist/docs/content/README.md +43 -44
- package/dist/docs/content/boundaries.mdx +141 -48
- package/dist/docs/content/cli/commands.mdx +161 -107
- package/dist/docs/content/cli/overview.mdx +61 -46
- package/dist/docs/content/cli/usage.mdx +107 -30
- package/dist/docs/content/config.mdx +145 -147
- package/dist/docs/content/controllers.mdx +193 -464
- package/dist/docs/content/cron.mdx +183 -69
- package/dist/docs/content/decorators.mdx +449 -154
- package/dist/docs/content/errors.mdx +202 -305
- package/dist/docs/content/exisjs-mcp.mdx +59 -49
- package/dist/docs/content/installation.mdx +243 -157
- package/dist/docs/content/introduction.mdx +220 -48
- package/dist/docs/content/middleware.mdx +171 -497
- package/dist/docs/content/modules.mdx +141 -83
- package/dist/docs/content/providers.mdx +216 -180
- package/dist/docs/content/requests.mdx +168 -488
- package/dist/docs/content/responses.mdx +200 -533
- package/dist/docs/content/routing.mdx +239 -558
- package/dist/docs/content/security.mdx +233 -57
- package/dist/docs/content/structure.mdx +151 -240
- package/dist/docs/content/swagger.mdx +192 -91
- package/dist/docs/content/testing.mdx +205 -74
- package/dist/docs/content/validation.mdx +479 -341
- package/dist/docs/reference/README.md +34 -2
- package/dist/docs/reference/config.mdx +277 -73
- package/dist/docs/reference/context.mdx +197 -106
- package/dist/docs/reference/cron.mdx +266 -152
- package/dist/docs/reference/database.mdx +135 -53
- package/dist/docs/reference/decorators.mdx +234 -116
- package/dist/docs/reference/di.mdx +146 -112
- package/dist/docs/reference/error.mdx +155 -178
- package/dist/docs/reference/exis.mdx +141 -70
- package/dist/docs/reference/logger.mdx +110 -126
- package/dist/docs/reference/middleware.mdx +235 -158
- package/dist/docs/reference/plugin.mdx +136 -64
- package/dist/docs/reference/router.mdx +277 -251
- package/dist/docs/reference/sanitize.mdx +91 -87
- package/dist/docs/reference/swagger.mdx +280 -100
- package/dist/docs/reference/testing.mdx +223 -89
- package/dist/docs/reference/validator.mdx +194 -206
- package/dist/lib/inspect-routes.d.ts +2 -0
- package/dist/lib/inspect-routes.d.ts.map +1 -0
- package/dist/lib/inspect-routes.js +148 -0
- package/dist/lib/inspect-routes.js.map +1 -0
- package/dist/lib/start-server.js +13 -8
- package/dist/lib/start-server.js.map +1 -1
- package/dist/module/module.d.ts +6 -3
- package/dist/module/module.d.ts.map +1 -1
- package/dist/module/module.js +17 -3
- package/dist/module/module.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +26 -4
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/router/scanner/cron-mounter.d.ts.map +1 -1
- package/dist/router/scanner/cron-mounter.js +9 -2
- package/dist/router/scanner/cron-mounter.js.map +1 -1
- package/dist/server/app.d.ts +3 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +29 -8
- package/dist/server/app.js.map +1 -1
- package/dist/server/define.d.ts.map +1 -1
- package/dist/server/define.js +5 -0
- package/dist/server/define.js.map +1 -1
- package/dist/server/request.d.ts +2 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +11 -0
- package/dist/server/request.js.map +1 -1
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/swagger/generator.js +130 -10
- package/dist/swagger/generator.js.map +1 -1
- package/dist/telemetry/index.d.ts +12 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +121 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/testing/client.d.ts +1 -0
- package/dist/testing/client.d.ts.map +1 -1
- package/dist/testing/client.js +4 -1
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +20 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/validator/builder.d.ts +26 -1
- package/dist/validator/builder.d.ts.map +1 -1
- package/dist/validator/builder.js +31 -0
- package/dist/validator/builder.js.map +1 -1
- package/dist/validator/engine.d.ts +23 -0
- package/dist/validator/engine.d.ts.map +1 -1
- package/dist/validator/engine.js +228 -31
- package/dist/validator/engine.js.map +1 -1
- package/dist/validator/index.d.ts +8 -4
- package/dist/validator/index.d.ts.map +1 -1
- package/dist/validator/index.js +4 -1
- package/dist/validator/index.js.map +1 -1
- package/dist/validator/tex-types.d.ts +15 -0
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +102 -0
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +1 -1
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +3 -1
- package/dist/validator/tex.js.map +1 -1
- package/dist/validator/types.d.ts +1 -0
- package/dist/validator/types.d.ts.map +1 -1
- package/package.json +47 -14
- package/dist/docs/content/dataloader.mdx +0 -170
- package/dist/docs/reference/dataloader.mdx +0 -91
|
@@ -1,522 +1,244 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
import { createRouter } from 'exisjs/router'
|
|
38
|
+
## Where to Apply Middleware
|
|
106
39
|
|
|
107
|
-
|
|
108
|
-
user: { id: string; role: 'admin' }
|
|
109
|
-
}
|
|
40
|
+
### 1. Global Middleware (`src/http/server.ts`)
|
|
110
41
|
|
|
111
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
55
|
+
### 2. Folder-Scoped Middleware (`src/http/**/boundary.ts`)
|
|
124
56
|
|
|
125
|
-
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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/
|
|
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('/
|
|
362
|
-
export default class
|
|
104
|
+
@Controller('/reports')
|
|
105
|
+
export default class ReportsController {
|
|
363
106
|
@Get('/')
|
|
364
|
-
@Use(
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
124
|
+
### 1. Declaring a Typed Middleware
|
|
391
125
|
|
|
392
|
-
```typescript filename="
|
|
393
|
-
import
|
|
126
|
+
```typescript filename="src/common/middleware/auth.ts"
|
|
127
|
+
import { defineMiddleware } from 'exisjs/router'
|
|
128
|
+
import { UnauthorizedException } from 'exisjs/error'
|
|
394
129
|
|
|
395
|
-
|
|
396
|
-
|
|
130
|
+
export interface AuthContext {
|
|
131
|
+
user: {
|
|
132
|
+
id: string
|
|
133
|
+
role: 'admin' | 'member'
|
|
134
|
+
email: string
|
|
135
|
+
}
|
|
397
136
|
}
|
|
398
137
|
|
|
399
|
-
export
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
### Route-Level Timeout Overrides
|
|
154
|
+
### 2. Using Typed Middleware in Functional Routes
|
|
430
155
|
|
|
431
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
177
|
+
## Multipart File Uploads (`fileUpload`)
|
|
178
|
+
|
|
179
|
+
ExisJS includes a streaming multipart parser in `exisjs/middleware` for file uploads:
|
|
473
180
|
|
|
474
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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
|
|
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('/
|
|
220
|
+
@Post('/avatar')
|
|
505
221
|
@UseUpload({
|
|
506
|
-
field: '
|
|
507
|
-
maxSize: '
|
|
222
|
+
field: 'avatar',
|
|
223
|
+
maxSize: '5MB',
|
|
508
224
|
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
509
225
|
destination: 'memory',
|
|
510
226
|
})
|
|
511
|
-
async
|
|
512
|
-
return {
|
|
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
|
-
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Express and Connect Middleware Bridge (`fromExpress`)
|
|
518
240
|
|
|
519
|
-
|
|
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 =
|
|
533
|
-
|
|
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
|
-
|
|
264
|
+
---
|
|
546
265
|
|
|
547
|
-
|
|
266
|
+
## Static File Serving (`serveStatic`)
|
|
548
267
|
|
|
549
|
-
|
|
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 {
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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.
|