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,709 +1,390 @@
1
1
  ---
2
- nextUrl: "/docs/controllers"
3
- nextTitle: "Controllers"
2
+ title: 'Routing & Endpoints'
3
+ description: 'File-system routing conventions, HTTP methods, dynamic route segments, catch-all parameters, and route options.'
4
+ nextUrl: '/docs/controllers'
5
+ nextTitle: 'Controllers'
4
6
  ---
5
7
 
6
- # Routing
8
+ # Routing & Endpoints
7
9
 
8
- Routes are responsible for handling incoming requests and sending responses back to the client. The routing mechanism determines which handler will process each request.
10
+ ExisJS uses file-system routing to map directory structures directly to HTTP endpoints. Dropping a `route.ts` or `<name>.route.ts` file inside a directory registers endpoints automatically with the native routing engine.
9
11
 
10
- <Info title="Functional vs OOP">
11
- ExisJS natively supports both a **Functional Paradigm** and a **Class-Based OOP Paradigm** (for dependency injection and decorators). You can mix and match them throughout your application.
12
- </Info>
13
-
14
- ## Folder-Based Routing
15
-
16
- ExisJS uses **folder-based file-system routing** instead of programmatic route declaration in a single entry file.
17
-
18
- For a folder to become an accessible API endpoint, it must contain a `route.ts` or `<feature>.route.ts` (e.g. `user.route.ts`) file that exports a route definition.
19
-
20
- In addition to `route.ts`, the ExisJS router utilizes two other special gatekeeper files:
21
- - **`server.ts`**: Placed at the root of your `http` directory, it is the entry point that boots your application, registers global plugins, and connects to databases.
22
- - **`boundary.ts`** (or `*.boundary.ts`): Can be placed in any folder. It acts as a gatekeeper and request pipeline, automatically configuring middleware, CORS, headers, and dependencies for *all* routes within its directory and subdirectories.
23
-
24
- ### Routing Conventions
25
-
26
- The ExisJS file-system router natively supports advanced folder naming conventions to handle dynamic segments and logical grouping:
27
-
28
- - **Dynamic Parameters (`[param]`)**: A folder named `[id]` creates a dynamic route segment (e.g., `/:id`). The value is accessible in your handler via `ctx.params.id`.
29
- - **Catch-all Routes (`[...param]`)**: A folder named `[...slug]` catches all subsequent URL segments, matching anything that follows (e.g., `/*slug`).
30
- - **Route Groups (`(group)`)**: A folder wrapped in parentheses (e.g., `(admin)`) is ignored in the final URL path. This is useful for grouping related routes or applying shared controller-level middleware without adding `/admin` to the URL.
31
-
32
- ### The Application Entry Point (`server.ts`)
33
-
34
- The `server.ts` file acts as the root configuration for your application. It provides lifecycle hooks to boot databases, register global plugins, and provide root-level dependencies.
35
-
36
- #### Functional Paradigm
37
- ```typescript filename="src/http/server.ts"
38
- import { exis } from 'exisjs'
39
-
40
- export default exis({
41
- providers: [
42
- ['LoggerService', { useValue: console }]
43
- ]
44
- })
45
- ```
46
-
47
- #### Class-Based (OOP) Paradigm
48
- ```typescript filename="src/http/server.ts"
49
- import { Server } from 'exisjs/decorators'
50
- import type { App } from 'exisjs'
51
-
52
- @Server({
53
- providers: [['LoggerService', { useValue: console }]]
54
- })
55
- export default class RootServer {
56
- async onStart(app: App) {
57
- // Boot your database here!
58
- }
59
- }
60
- ```
61
-
62
- ### Folder Boundaries (`boundary.ts`)
63
-
64
- Boundaries apply configurations, security policies, and request pipelines to all routes in their directory and subdirectories.
65
-
66
- #### Functional Paradigm
67
- ```typescript filename="src/http/admin/boundary.ts"
68
- import { defineBoundary } from 'exisjs/router'
69
-
70
- export const config = defineBoundary({
71
- headers: { 'X-Admin-Area': 'true' }
72
- })
73
- ```
74
-
75
- #### Class-Based (OOP) Paradigm
76
- ```typescript filename="src/http/admin/boundary.ts"
77
- import { Boundary } from 'exisjs/decorators'
78
-
79
- @Boundary({
80
- headers: { 'X-Admin-Area': 'true' }
81
- })
82
- export default class AdminBoundary {}
83
- ```
84
-
85
- ### Route Definitions (`route.ts`)
86
-
87
- For example, to create a simple `/health` endpoint, you create the folder `src/http/health/` and place a `route.ts` file inside it:
88
-
89
- #### Functional Paradigm
12
+ ---
90
13
 
14
+ ## Routing Principles
91
15
 
92
- ```typescript filename="src/http/health/route.ts"
93
- import { controller, route } from 'exisjs/router'
16
+ 1. **Folder-Based URL Mapping**: The folder structure relative to `src/http/` determines the base URL path.
17
+ 2. **Dual-Paradigm Support**: Routes can be authored using either the **Functional Paradigm** (`controller()`, `route.*`) or the **Class-Based (OOP) Paradigm** (`@Controller()`, `@Get()`, `@Post()`). Choose one paradigm per project.
18
+ 3. **Automatic Parameter Inference**: Dynamic URL segments defined in path strings (such as `/:id` or `/:postId`) or folder names (`[id]`) automatically populate typed route parameters in handler contexts.
94
19
 
95
- export default controller({
96
- check: route.get('/', {
97
- handle() {
98
- return { status: 'ok', timestamp: new Date().toISOString() }
99
- }
100
- })
101
- })
102
- ```
103
-
104
-
105
- #### Class-Based (OOP) Paradigm
20
+ ---
106
21
 
22
+ ## File-System Route Conventions
107
23
 
108
- ```typescript filename="src/http/health/route.ts"
109
- import { Controller, Get } from 'exisjs/decorators'
24
+ | Directory Path | URL Endpoint | Description |
25
+ | :--- | :--- | :--- |
26
+ | `src/http/route.ts` | `/` | Root application endpoint |
27
+ | `src/http/health/route.ts` | `/health` | Static route endpoint |
28
+ | `src/http/users/route.ts` | `/users` | Collection endpoint |
29
+ | `src/http/users/[id]/route.ts` | `/users/:id` | Dynamic parameter route |
30
+ | `src/http/docs/[...slug]/route.ts` | `/docs/*slug` | Catch-all wildcard route |
31
+ | `src/http/(admin)/dashboard/route.ts` | `/dashboard` | Route group (parentheses are omitted from the URL path) |
110
32
 
111
- @Controller()
112
- export default class HealthController {
113
- @Get('/')
114
- check() {
115
- return { status: 'ok', timestamp: new Date().toISOString() }
116
- }
117
- }
118
- ```
119
-
120
-
33
+ ---
121
34
 
122
- ## Supported Methods
35
+ ## Route Definitions
123
36
 
124
- ExisJS natively exposes standard HTTP methods alongside advanced routing paradigms like WebSockets and Server-Sent Events. Choose your paradigm below to see the available methods.
37
+ ### Basic Route Handlers
125
38
 
126
39
  #### Functional Paradigm
127
40
 
128
- <div className="overflow-x-auto mb-6">
129
- <table className="w-full text-sm text-left border border-border/50 rounded-lg overflow-hidden">
130
- <thead className="bg-muted/50 text-foreground">
131
- <tr>
132
- <th className="px-4 py-3 border-b border-border/50 font-semibold w-1/3">Method</th>
133
- <th className="px-4 py-3 border-b border-border/50 font-semibold">Description</th>
134
- </tr>
135
- </thead>
136
- <tbody className="divide-y divide-border/50">
137
- <tr className="bg-background hover:bg-muted/30 transition-colors">
138
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">route.</span>get() / post() / put()</td>
139
- <td className="px-4 py-3 text-muted-foreground">Standard HTTP methods. Also includes patch, delete, options, head, connect, and trace.</td>
140
- </tr>
141
- <tr className="bg-background hover:bg-muted/30 transition-colors">
142
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">route.</span>all()</td>
143
- <td className="px-4 py-3 text-muted-foreground">Matches any HTTP request method for the given path.</td>
144
- </tr>
145
- <tr className="bg-background hover:bg-muted/30 transition-colors">
146
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">route.</span>query()</td>
147
- <td className="px-4 py-3 text-muted-foreground">The new experimental HTTP QUERY method, solving complex idempotent fetches.</td>
148
- </tr>
149
- <tr className="bg-background hover:bg-muted/30 transition-colors">
150
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">route.</span>ws()</td>
151
- <td className="px-4 py-3 text-muted-foreground">Exposes a native WebSocket endpoint with automatic pub/sub support.</td>
152
- </tr>
153
- <tr className="bg-background hover:bg-muted/30 transition-colors">
154
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">route.</span>sse()</td>
155
- <td className="px-4 py-3 text-muted-foreground">Exposes a Server-Sent Events stream for one-way realtime data.</td>
156
- </tr>
157
- </tbody>
158
- </table>
159
- </div>
160
-
161
41
  ```typescript filename="src/http/users/route.ts"
162
42
  import { controller, route } from 'exisjs/router'
163
43
 
164
44
  export default controller({
165
- list: route.get('/', {
166
- handle() {
167
- // Fetch all users from database
45
+ // GET /users
46
+ listUsers: route.get('/', {
47
+ summary: 'List users',
48
+ handle: async () => {
168
49
  return [
169
- { id: 1, name: 'Alice' },
170
- { id: 2, name: 'Bob' }
50
+ { id: '1', name: 'Alice' },
51
+ { id: '2', name: 'Bob' },
171
52
  ]
172
- }
53
+ },
173
54
  }),
174
- create: route.post('/', {
175
- handle({ req }) {
176
- // Create a new user with parsed payload
177
- const payload = req.body
178
- return { id: 3, ...payload }
179
- }
55
+
56
+ // POST /users
57
+ createUser: route.post('/', {
58
+ summary: 'Create user',
59
+ handle: async ({ body }) => {
60
+ return { id: '3', ...body }
61
+ },
180
62
  }),
181
- preflight: route.options('/', {
182
- handle({ res }) {
183
- // Handle custom preflight logic if `preflightContinue: true` is configured in CORS
184
- res.header('X-Custom-Options', 'true')
185
- return { allowed: true }
186
- }
187
- })
188
63
  })
189
64
  ```
190
-
191
- #### Class-Based (OOP) Paradigm
192
65
 
193
- <div className="overflow-x-auto mb-6">
194
- <table className="w-full text-sm text-left border border-border/50 rounded-lg overflow-hidden">
195
- <thead className="bg-muted/50 text-foreground">
196
- <tr>
197
- <th className="px-4 py-3 border-b border-border/50 font-semibold w-1/3">Decorator</th>
198
- <th className="px-4 py-3 border-b border-border/50 font-semibold">Description</th>
199
- </tr>
200
- </thead>
201
- <tbody className="divide-y divide-border/50">
202
- <tr className="bg-background hover:bg-muted/30 transition-colors">
203
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Get() / <span className="text-primary">@</span>Post() / <span className="text-primary">@</span>Put()</td>
204
- <td className="px-4 py-3 text-muted-foreground">Standard HTTP methods. Also includes @Patch, @Delete, @Options, @Head, @Connect, and @Trace.</td>
205
- </tr>
206
- <tr className="bg-background hover:bg-muted/30 transition-colors">
207
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>All()</td>
208
- <td className="px-4 py-3 text-muted-foreground">Matches any HTTP request method for the given path.</td>
209
- </tr>
210
- <tr className="bg-background hover:bg-muted/30 transition-colors">
211
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Query()</td>
212
- <td className="px-4 py-3 text-muted-foreground">The new experimental HTTP QUERY method, solving complex idempotent fetches.</td>
213
- </tr>
214
- <tr className="bg-background hover:bg-muted/30 transition-colors">
215
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Ws()</td>
216
- <td className="px-4 py-3 text-muted-foreground">Exposes a native WebSocket endpoint with automatic pub/sub support.</td>
217
- </tr>
218
- <tr className="bg-background hover:bg-muted/30 transition-colors">
219
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Sse()</td>
220
- <td className="px-4 py-3 text-muted-foreground">Exposes a Server-Sent Events stream for one-way realtime data.</td>
221
- </tr>
222
- </tbody>
223
- </table>
224
- </div>
66
+ #### Class-Based (OOP) Paradigm
225
67
 
226
68
  ```typescript filename="src/http/users/route.ts"
227
- import { Controller, Get, Post, Body } from 'exisjs/decorators'
69
+ import { Controller, Get, Post, Body, ApiOperation } from 'exisjs/decorators'
228
70
 
229
71
  @Controller()
230
72
  export default class UsersController {
231
73
  @Get('/')
232
- list() {
233
- // Fetch all users from database
74
+ @ApiOperation({ summary: 'List users' })
75
+ async listUsers() {
234
76
  return [
235
- { id: 1, name: 'Alice' },
236
- { id: 2, name: 'Bob' }
77
+ { id: '1', name: 'Alice' },
78
+ { id: '2', name: 'Bob' },
237
79
  ]
238
80
  }
239
81
 
240
82
  @Post('/')
241
- create(@Body() data: any) {
242
- // Create a new user with parsed payload
243
- return { id: 3, ...data }
83
+ @ApiOperation({ summary: 'Create user' })
84
+ async createUser(@Body() body: Record<string, any>) {
85
+ return { id: '3', ...body }
244
86
  }
245
87
  }
246
88
  ```
247
-
248
-
249
- ## Sub-Paths in a Single File
250
-
251
- While folder-based routing is standard, you can also define multiple sub-paths in a single `route.ts` file to group related endpoints.
252
-
253
- #### Functional Paradigm
254
-
255
-
256
- ```typescript filename="src/http/users/route.ts"
257
- import { controller, route } from 'exisjs/router'
258
-
259
- export default controller({
260
- list: route.get('/', {
261
- handle() {
262
- return { message: 'Fetching all users' }
263
- }
264
- }),
265
- getSingle: route.get('/:id', {
266
- handle({ params }) {
267
- // params.id is automatically inferred as string with full autocomplete!
268
- const { id } = params
269
- return { message: `Fetching user ${id}` }
270
- }
271
- })
272
- })
273
- ```
274
-
275
- <Info title="Automatic Route Parameter Type Inference">
276
- In ExisJS v0.7+, paths with named parameters like `route.get('/users/:userId/posts/:postId', ...)` automatically parse the path string literal to infer `params: { userId: string; postId: string }` on the `handle` context. No manual casting or boilerplate interfaces required.
277
- </Info>
278
89
 
279
-
280
- #### Class-Based (OOP) Paradigm
90
+ ---
281
91
 
92
+ ## Supported HTTP Methods
282
93
 
283
- ```typescript filename="src/http/users/route.ts"
284
- import { Controller, Get, Param } from 'exisjs/decorators'
94
+ ExisJS supports all standard HTTP methods across both paradigms:
285
95
 
286
- @Controller()
287
- export default class UsersController {
288
- @Get('/')
289
- list() {
290
- return { message: 'Fetching all users' }
291
- }
96
+ | HTTP Verb | Functional Method | OOP Decorator | Description |
97
+ | :--- | :--- | :--- | :--- |
98
+ | `GET` | `route.get(path, config)` | `@Get(path, schema)` | Retrieve resources |
99
+ | `POST` | `route.post(path, config)` | `@Post(path, schema)` | Create resources or trigger actions |
100
+ | `PUT` | `route.put(path, config)` | `@Put(path, schema)` | Replace existing resources |
101
+ | `PATCH` | `route.patch(path, config)` | `@Patch(path, schema)` | Apply partial updates |
102
+ | `DELETE` | `route.delete(path, config)` | `@Delete(path, schema)` | Remove resources |
103
+ | `OPTIONS` | `route.options(path, config)` | `@Options(path, schema)` | Describe communication options / preflight |
104
+ | `HEAD` | `route.head(path, config)` | `@Head(path, schema)` | Retrieve headers identical to a GET request |
105
+ | `CONNECT` | `route.connect(path, config)` | `@Connect(path, schema)` | Establish a two-way tunnel |
106
+ | `TRACE` | `route.trace(path, config)` | `@Trace(path, schema)` | Loop-back testing along network path |
107
+ | `QUERY` | `route.query(path, config)` | `@Query(path, schema)` | Safe query method with request payload (RFC 9535) |
108
+ | `ALL` | `route.all(path, config)` | `@All(path, schema)` | Matches any HTTP method for the given path |
292
109
 
293
- @Get('/:id')
294
- getSingle(@Param('id') id: string) {
295
- return { message: \`Fetching user \${id}\` }
296
- }
297
- }
298
- ```
110
+ ---
299
111
 
300
-
112
+ ## Dynamic Route Parameters
301
113
 
302
- ## Route Parameters (Dynamic Routing)
114
+ ### Folder-Based Parameters (`[param]`)
303
115
 
304
- Routes with static paths won’t work when you need to accept dynamic data as part of the request (e.g., `GET /users/123`). To create dynamic routes using folders, enclose the folder name in brackets `[]`.
116
+ Creating a folder named `[id]` creates a parameterized route segment.
305
117
 
306
118
  #### Functional Paradigm
307
119
 
308
-
309
120
  ```typescript filename="src/http/users/[id]/route.ts"
310
- // Location: src/http/users/[id]/route.ts
311
121
  import { controller, route } from 'exisjs/router'
122
+ import { tex } from 'exisjs/validator'
123
+
124
+ const UserParamsSchema = tex.object({
125
+ id: tex.string({ min: 1 }),
126
+ })
312
127
 
313
128
  export default controller({
314
- getSingle: route.get('/', {
315
- handle({ params }) {
316
- const { id } = params
317
- return { message: \`Fetching user \${id}\` }
318
- }
319
- })
129
+ getUser: route.get('/', {
130
+ params: UserParamsSchema,
131
+ handle: async ({ params }) => {
132
+ return { id: params.id, name: 'Alice' }
133
+ },
134
+ }),
320
135
  })
321
136
  ```
322
137
 
323
-
324
138
  #### Class-Based (OOP) Paradigm
325
139
 
326
-
327
140
  ```typescript filename="src/http/users/[id]/route.ts"
328
- // Location: src/http/users/[id]/route.ts
329
- import { Controller, Get, Param } from 'exisjs/decorators'
141
+ import { Controller, Get, Param, ApiOperation } from 'exisjs/decorators'
142
+ import { tex } from 'exisjs/validator'
143
+
144
+ const UserParamsSchema = tex.object({
145
+ id: tex.string({ min: 1 }),
146
+ })
330
147
 
331
148
  @Controller()
332
149
  export default class SingleUserController {
333
- @Get('/')
334
- getSingle(@Param('id') id: string) {
335
- return { message: \`Fetching user \${id}\` }
150
+ @Get('/', { params: UserParamsSchema })
151
+ @ApiOperation({ summary: 'Get user by ID' })
152
+ async getUser(@Param('id') id: string) {
153
+ return { id, name: 'Alice' }
336
154
  }
337
155
  }
338
156
  ```
339
157
 
340
-
341
-
342
- ## Request & Response Objects
343
-
344
- Handlers often need access to the client’s request details, or fine-grained control over the response headers and status codes. Every route handler natively receives powerful `req` and `res` objects.
158
+ ---
345
159
 
346
- #### Functional Paradigm
160
+ ### In-File Sub-Paths
347
161
 
162
+ You can also define multiple sub-paths with route parameters inside a single `route.ts` file:
348
163
 
349
- ```typescript filename="src/http/profile/route.ts"
164
+ ```typescript filename="src/http/users/route.ts"
350
165
  import { controller, route } from 'exisjs/router'
166
+ import { tex } from 'exisjs/validator'
351
167
 
352
168
  export default controller({
353
- profile: route.get('/profile', {
354
- handle({ req, res }) {
355
- // Read Request details
356
- const ip = req.ip
357
- const headers = req.headers
358
-
359
- // Chain Response methods
360
- res
361
- .status(202)
362
- .cookie('session', 'xyz123', { httpOnly: true, secure: true })
363
- .header('X-Custom-Header', 'Exis')
364
- .json({ message: 'Accepted' })
365
- }
366
- })
367
- })
368
- ```
369
-
370
-
371
- #### Class-Based (OOP) Paradigm
372
-
169
+ // GET /users
170
+ list: route.get('/', {
171
+ handle: async () => [{ id: '1', name: 'Alice' }],
172
+ }),
373
173
 
374
- ```typescript filename="src/http/profile/route.ts"
375
- import { Controller, Get, Req, Res } from 'exisjs/decorators'
376
- import type { Request, Response } from 'exisjs/router'
174
+ // GET /users/:id
175
+ getById: route.get('/:id', {
176
+ params: tex.object({ id: tex.string() }),
177
+ handle: async ({ params }) => ({ id: params.id, name: 'Alice' }),
178
+ }),
377
179
 
378
- @Controller()
379
- export default class ProfileController {
380
- @Get('/profile')
381
- profile(@Req() req: Request, @Res() res: Response) {
382
- // Read Request details
383
- const ip = req.ip
384
- const headers = req.headers
385
-
386
- // Chain Response methods
387
- res
388
- .status(202)
389
- .cookie('session', 'xyz123', { httpOnly: true, secure: true })
390
- .header('X-Custom-Header', 'Exis')
391
- .json({ message: 'Accepted' })
392
- }
393
- }
180
+ // GET /users/:id/posts/:postId
181
+ getUserPost: route.get('/:id/posts/:postId', {
182
+ params: tex.object({
183
+ id: tex.string(),
184
+ postId: tex.string(),
185
+ }),
186
+ handle: async ({ params }) => {
187
+ return { userId: params.id, postId: params.postId, title: 'Hello ExisJS' }
188
+ },
189
+ }),
190
+ })
394
191
  ```
395
192
 
396
-
397
-
398
- <Info title="Default Responses">
399
- When a request handler returns a JavaScript object or array, it is automatically serialized to JSON and sent with a default `200 OK` status code. You only need to use the `res` object when you want to explicitly modify headers, cookies, or status codes!
400
- </Info>
401
-
402
- ## Payloads & Query Parameters
403
-
404
- You can easily extract query strings from the URL or parsed JSON bodies from POST requests directly through the `req` object (or via decorators in OOP).
193
+ ---
405
194
 
406
- #### Functional Paradigm
195
+ ## Catch-All Routes (`[...slug]`)
407
196
 
197
+ Folders named `[...slug]` match all subsequent nested URL paths.
408
198
 
409
- ```typescript filename="src/http/search/route.ts"
199
+ ```typescript filename="src/http/docs/[...slug]/route.ts"
410
200
  import { controller, route } from 'exisjs/router'
411
201
 
412
202
  export default controller({
413
- search: route.get('/search', {
414
- handle({ req }) {
415
- // e.g., GET /search?q=exis&limit=10
416
- const { q, limit } = req.query
417
- return { query: q, limit }
418
- }
203
+ renderDoc: route.get('/', {
204
+ handle: async ({ params }) => {
205
+ // For a request to /docs/getting-started/installation
206
+ // params.slug contains "getting-started/installation"
207
+ return { slug: params.slug }
208
+ },
419
209
  }),
420
- create: route.post('/create', {
421
- handle({ req }) {
422
- // Access parsed JSON body natively
423
- const data = req.body
424
- return { received: data }
425
- }
426
- })
427
210
  })
428
211
  ```
429
212
 
430
-
431
- #### Class-Based (OOP) Paradigm
213
+ ---
432
214
 
215
+ ## Route Groups (`(group)`)
433
216
 
434
- ```typescript filename="src/http/search/route.ts"
435
- import { Controller, Get, Post, Query, Body } from 'exisjs/decorators'
217
+ Parenthesized directory names group routes logically without appending the folder name to the URL path:
436
218
 
437
- @Controller()
438
- export default class SearchController {
439
- @Get('/search')
440
- search(@Query('q') q: string, @Query('limit') limit: number) {
441
- // e.g., GET /search?q=exis&limit=10
442
- return { query: q, limit }
443
- }
444
-
445
- @Post('/create')
446
- create(@Body() data: any) {
447
- // Access parsed JSON body natively
448
- return { received: data }
449
- }
450
- }
219
+ ```
220
+ src/http/
221
+ ├── (auth)/
222
+ │ ├── login/
223
+ │ │ └── route.ts # Maps to /login (not /auth/login)
224
+ │ └── register/
225
+ │ └── route.ts # Maps to /register (not /auth/register)
226
+ └── (marketing)/
227
+ └── pricing/
228
+ └── route.ts # Maps to /pricing (not /marketing/pricing)
451
229
  ```
452
230
 
453
-
454
-
455
- ## Declarative Route Options (Uploads, Timeouts & Validation)
456
-
457
- ExisJS allows configuring input validation schemas, multipart file uploads, and per-route timeouts directly inside the route definition without manual middleware plumbing.
231
+ ---
458
232
 
459
- ### Declarative File Uploads
233
+ ## Handler Execution Context (`SuperContext`)
460
234
 
461
- Specify `upload` in your route definition to automatically handle `multipart/form-data`. Uploaded files are parsed and attached to `req.file` or `req.files`, while non-file form fields are populated in `req.body`.
462
-
463
- #### Functional Paradigm
464
- ```typescript filename="src/http/avatar/route.ts"
465
- import { controller, route } from 'exisjs/router'
466
- import { tex } from 'exisjs/validator'
235
+ In the functional paradigm, `handle` receives a single context object containing all request properties and utilities:
467
236
 
237
+ ```typescript
468
238
  export default controller({
469
- uploadAvatar: route.post('/', {
470
- upload: {
471
- field: 'avatar',
472
- maxBytes: 5 * 1024 * 1024, // 5MB limit
473
- mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
239
+ endpoint: route.post('/example', {
240
+ handle: async (ctx) => {
241
+ const { body, query, params, headers, req, res, app, resolve, file } = ctx
242
+ // ...
474
243
  },
475
- body: tex.object({
476
- caption: tex.string({ optional: true }),
477
- }),
478
- handle({ req }) {
479
- const file = req.file! // UploadedFile { filename, buffer, size, mimetype }
480
- const caption = req.body?.caption
481
-
482
- return {
483
- success: true,
484
- filename: file.filename,
485
- size: file.size,
486
- caption,
487
- }
488
- }
489
- })
244
+ }),
490
245
  })
491
246
  ```
492
247
 
493
- #### Class-Based (OOP) Paradigm
494
- ```typescript filename="src/http/avatar/route.ts"
495
- import { Controller, Post, Body } from 'exisjs/decorators'
496
- import { Upload, UploadedFile } from 'exisjs/decorators'
497
- import type { UploadedFile as UploadedFileType } from 'exisjs/middleware'
498
-
499
- @Controller('/avatar')
500
- export default class AvatarController {
501
- @Post('/')
502
- @Upload('avatar', {
503
- maxBytes: 5 * 1024 * 1024,
504
- mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
505
- })
506
- upload(
507
- @UploadedFile() file: UploadedFileType,
508
- @Body('caption') caption?: string
509
- ) {
510
- return {
511
- success: true,
512
- filename: file.filename,
513
- size: file.size,
514
- caption,
515
- }
516
- }
517
- }
518
- ```
248
+ ### Context Properties
249
+
250
+ | Property | Type | Description |
251
+ | :--- | :--- | :--- |
252
+ | `ctx.body` | `TBody` | Parsed and validated request body payload |
253
+ | `ctx.query` | `TQuery` | Parsed and validated URL query parameters |
254
+ | `ctx.params` | `TParams` | Extracted route parameters |
255
+ | `ctx.headers` | `Record<string, string>` | Request headers map |
256
+ | `ctx.req` | `Request` | The underlying `ExisRequest` instance |
257
+ | `ctx.res` | `Response` | The underlying `ExisResponse` instance |
258
+ | `ctx.resolve` | `(token) => T` | Resolves a dependency from the IoC container |
259
+ | `ctx.file` | `ExisFile` | Uploaded file instance (when multipart upload is enabled) |
260
+ | `ctx.files` | `ExisFile[]` | Uploaded files array (for multiple file uploads) |
261
+ | `ctx.state` | `Record<string, any>` | Request-scoped custom state |
519
262
 
520
- ### Route-Level Timeout Overrides
263
+ ---
521
264
 
522
- While ExisJS provides a generous 60-second default global timeout, long-running batch jobs, PDF generation, or third-party AI integrations can override the timeout at the individual route level:
265
+ ## Declarative Route Configuration Options
523
266
 
524
- #### Functional Paradigm
525
- ```typescript filename="src/http/reports/route.ts"
526
- import { controller, route } from 'exisjs/router'
267
+ Each route handler accepts an options object supporting validation, security policies, file uploads, and timeouts:
527
268
 
528
- export default controller({
529
- generateAnnualReport: route.post('/annual', {
530
- timeout: 120000, // 2 minutes specifically for this route
531
- async handle() {
532
- // Intensive document rendering or AI workflow
533
- const reportUrl = await buildReport()
534
- return { success: true, reportUrl }
535
- }
536
- })
537
- })
538
- ```
539
-
540
- ### Query Pagination Schemas
541
-
542
- ExisJS provides the `tex.pagination()` primitive for declarative pagination, sorting, and ordering query parameters. String query parameters are automatically coerced into numbers:
269
+ ### 1. Request Validation Schemas
543
270
 
544
271
  ```typescript filename="src/http/products/route.ts"
545
272
  import { controller, route } from 'exisjs/router'
546
- import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
273
+ import { tex } from 'exisjs/validator'
547
274
 
548
- const ProductQuerySchema = tex.pagination({
549
- defaultLimit: 20,
550
- maxLimit: 100,
275
+ const ProductQuerySchema = tex.object({
276
+ page: tex.number({ coerce: true, default: 1 }),
277
+ limit: tex.number({ coerce: true, default: 20 }),
278
+ })
279
+
280
+ const ProductBodySchema = tex.object({
281
+ name: tex.string({ min: 2 }),
282
+ price: tex.number({ min: 0 }),
551
283
  })
552
284
 
553
285
  export default controller({
554
- list: route.get('/', {
286
+ create: route.post('/', {
555
287
  query: ProductQuerySchema,
556
- async handle({ query }) {
557
- // query.page and query.limit are guaranteed positive numbers
558
- const { skip, limit, page } = getPaginationSkip(query)
559
-
560
- // Fetch data using your preferred database query
561
- const [items, total] = await Promise.all([
562
- db.products.findMany({ skip, take: limit }),
563
- db.products.count()
564
- ])
565
-
566
- return paginate(items, total, { page, limit })
567
- }
568
- })
288
+ body: ProductBodySchema,
289
+ handle: async ({ query, body }) => {
290
+ return { page: query.page, product: body }
291
+ },
292
+ }),
569
293
  })
570
294
  ```
571
295
 
572
- ## Dependency Injection in Functional Controllers
573
-
574
- ExisJS natively supports **Request-Scoped Dependency Injection** directly inside functional routes. You don't need to use classes to get the benefits of DI!
296
+ ---
575
297
 
576
- By destructuring `resolve` from the context, you can dynamically fetch services without relying on global imports. This perfectly solves the "Fat Handler" problem by encouraging you to abstract business logic into clean Service classes, and makes unit testing incredibly easy.
298
+ ### 2. Multipart File Uploads
577
299
 
578
- ```typescript filename="src/http/users/route.ts"
300
+ ```typescript filename="src/http/upload/route.ts"
579
301
  import { controller, route } from 'exisjs/router'
580
- import { UsersService } from './service'
581
302
 
582
303
  export default controller({
583
- list: route.get('/', {
584
- async handle({ resolve }) {
585
- // 1. Resolves UsersService from the IoC Container
586
- // 2. Maintains request-scope (the same instance is returned throughout this HTTP request)
587
- const usersService = resolve(UsersService)
588
-
589
- return usersService.getAll()
590
- }
591
- })
304
+ uploadFile: route.post('/', {
305
+ upload: {
306
+ field: 'file',
307
+ maxBytes: 5 * 1024 * 1024, // 5MB limit
308
+ mimeTypes: ['image/png', 'image/jpeg', 'application/pdf'],
309
+ },
310
+ handle: async ({ file }) => {
311
+ return {
312
+ filename: file?.filename,
313
+ size: file?.size,
314
+ mimeType: file?.mimeType,
315
+ }
316
+ },
317
+ }),
592
318
  })
593
319
  ```
594
320
 
595
- ## The Context API (Advanced)
596
-
597
- For applications where passing `(req, res)` arguments through multiple service layers becomes tedious, ExisJS offers a Next.js-inspired approach: **The Context API**. Powered by Node.js `AsyncLocalStorage`, ExisJS provides hooks to access the current request state from anywhere in the execution context.
321
+ ---
598
322
 
599
- #### Functional Paradigm
323
+ ### 3. Route-Specific Timeout Overrides
600
324
 
325
+ You can override default timeouts for long-running endpoints:
601
326
 
602
- ```typescript filename="src/http/users/route.ts"
603
- import { controller, route, getContext, getRequest, getResponse, after } from 'exisjs/router'
327
+ ```typescript filename="src/http/reports/route.ts"
328
+ import { controller, route } from 'exisjs/router'
604
329
 
605
330
  export default controller({
606
- create: route.post('/', {
607
- async handle() {
608
- // Access request/response without prop-drilling
609
- const req = getRequest()
610
- const res = getResponse()
611
-
612
- // Access data injected by gateways
613
- const { userId } = getContext<{ userId: string }>()
614
-
615
- // Run background tasks after sending response to client
616
- after(async () => {
617
- await sendNotificationEmail(userId)
618
- })
619
-
620
- res.status(201)
621
- return { success: true }
622
- }
623
- })
331
+ generateReport: route.post('/annual', {
332
+ timeout: 120000, // 120 seconds in milliseconds
333
+ handle: async () => {
334
+ // Perform long-running export
335
+ return { status: 'completed' }
336
+ },
337
+ }),
624
338
  })
625
339
  ```
626
340
 
627
-
628
- #### Class-Based (OOP) Paradigm
629
-
630
-
631
- ```typescript filename="src/http/users/route.ts"
632
- import { Controller, Post } from 'exisjs/decorators'
633
- import { getContext, getRequest, getResponse, after } from 'exisjs/router'
634
-
635
- @Controller()
636
- export default class OrdersController {
637
- @Post('/')
638
- async create() {
639
- // You can still use the Context API seamlessly in OOP classes!
640
- const req = getRequest()
641
- const res = getResponse()
642
- const { userId } = getContext<{ userId: string }>()
643
-
644
- after(async () => {
645
- await sendNotificationEmail(userId)
646
- })
647
-
648
- res.status(201)
649
- return { success: true }
650
- }
651
- }
652
- ```
653
-
654
-
655
-
656
- ## Type-Safe Client Route Manifest Contract (`AppRouter`)
341
+ ---
657
342
 
658
- ExisJS generates a compile-time route manifest in `.exis/types.d.ts` (and exposed via root `exis.d.ts`), converting your filesystem routes into an `AppRouter` type definition.
343
+ ### 4. Route-Specific CORS Overrides
659
344
 
660
- ### Zero-Cost Return Type & DTO Preservation
661
- When you return data from `handle()`, ExisJS preserves the exact TypeScript return type (including Drizzle/Prisma models, Tex schemas, or custom records) on the route definition:
345
+ Route-level CORS configuration takes precedence over boundary and global settings:
662
346
 
663
- ```typescript filename="src/http/users/route.ts"
347
+ ```typescript filename="src/http/webhooks/stripe/route.ts"
664
348
  import { controller, route } from 'exisjs/router'
665
- import { tex } from 'exisjs/validator'
666
349
 
667
350
  export default controller({
668
- getUser: route.get('/:id', {
669
- params: tex.object({ id: tex.string() }),
670
- async handle({ params }) {
671
- // Inferred return type is strictly preserved as { id: string, name: string, active: boolean }
672
- return { id: params.id, name: 'Alice', active: true }
673
- }
674
- })
351
+ stripeWebhook: route.post('/', {
352
+ cors: {
353
+ origin: 'https://hooks.stripe.com',
354
+ methods: ['POST'],
355
+ },
356
+ handle: async ({ body }) => {
357
+ return { received: true }
358
+ },
359
+ }),
675
360
  })
676
361
  ```
677
362
 
678
- ### Consuming `AppRouter` with `@exisjs/fetch`
679
- You can import `AppRouter` in your frontend or external client applications for zero-cost end-to-end type safety and autocomplete:
363
+ ---
680
364
 
681
- ```typescript filename="src/client/api.ts"
682
- import { createClient } from '@exisjs/fetch'
683
- import type { AppRouter } from '../server/.exis/types'
365
+ ## Dependency Resolution in Functional Routes
684
366
 
685
- const api = createClient<AppRouter>({
686
- baseUrl: 'http://localhost:4000',
687
- })
367
+ Functional routes can resolve dependencies directly from the IoC container using `ctx.resolve()`:
688
368
 
689
- // 1. Hierarchical dot-notation RPC call:
690
- const user = await api.users('123').getUser.get()
369
+ ```typescript filename="src/http/users/route.ts"
370
+ import { controller, route } from 'exisjs/router'
371
+ import { UserService } from './service'
691
372
 
692
- // 2. Direct path index call:
693
- const userByPath = await api['/users/:id'].getUser.get({ params: { id: '123' } })
373
+ export default controller({
374
+ list: route.get('/', {
375
+ handle: async ({ resolve }) => {
376
+ const userService = resolve(UserService)
377
+ return userService.findAll()
378
+ },
379
+ }),
380
+ })
694
381
  ```
695
382
 
696
- ### Dynamic Dev Watcher Sync
697
- During `exis dev`, adding, renaming, moving, or deleting route files automatically updates `.exis/types.d.ts` and `.exis/types/index.d.ts` in real-time without requiring a dev server restart or manual `.exis` folder deletion.
698
-
699
383
  ---
700
384
 
701
- ## Modular Architecture
702
-
703
- While putting all logic in `route.ts` is fine for small apps, ExisJS strongly encourages separating concerns alongside your route definition:
704
-
705
- - **`schema.ts`**: Defines expected input validation constraints.
706
- - **`service.ts`**: Pure functions that interact with your database.
707
- - **`controller.ts`**: Handles business orchestration.
708
- - **`route.ts`**: Wires it to the HTTP network.
385
+ ## Next Steps
709
386
 
387
+ - **[Controllers & Decorators](/docs/controllers)**: Complete guide to class-based routing and parameter decorators.
388
+ - **[Request Object](/docs/requests)**: Deep dive into the `ExisRequest` API.
389
+ - **[Response Object](/docs/responses)**: Status codes, cookie management, streaming, and Server-Sent Events.
390
+ - **[Boundaries](/docs/boundaries)**: Scoped middleware cascading, directory guards, and error boundaries.