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,709 +1,390 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
35
|
+
## Route Definitions
|
|
123
36
|
|
|
124
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
return { id: 3, ...
|
|
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
|
-
|
|
284
|
-
import { Controller, Get, Param } from 'exisjs/decorators'
|
|
94
|
+
ExisJS supports all standard HTTP methods across both paradigms:
|
|
285
95
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
+
### Folder-Based Parameters (`[param]`)
|
|
303
115
|
|
|
304
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return {
|
|
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
|
-
|
|
329
|
-
import {
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
195
|
+
## Catch-All Routes (`[...slug]`)
|
|
407
196
|
|
|
197
|
+
Folders named `[...slug]` match all subsequent nested URL paths.
|
|
408
198
|
|
|
409
|
-
```typescript filename="src/http/
|
|
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
|
-
|
|
414
|
-
handle({
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
return {
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
233
|
+
## Handler Execution Context (`SuperContext`)
|
|
460
234
|
|
|
461
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
263
|
+
---
|
|
521
264
|
|
|
522
|
-
|
|
265
|
+
## Declarative Route Configuration Options
|
|
523
266
|
|
|
524
|
-
|
|
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
|
-
|
|
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
|
|
273
|
+
import { tex } from 'exisjs/validator'
|
|
547
274
|
|
|
548
|
-
const ProductQuerySchema = tex.
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
286
|
+
create: route.post('/', {
|
|
555
287
|
query: ProductQuerySchema,
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
+
### 2. Multipart File Uploads
|
|
577
299
|
|
|
578
|
-
```typescript filename="src/http/
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
603
|
-
import { controller, route
|
|
327
|
+
```typescript filename="src/http/reports/route.ts"
|
|
328
|
+
import { controller, route } from 'exisjs/router'
|
|
604
329
|
|
|
605
330
|
export default controller({
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
343
|
+
### 4. Route-Specific CORS Overrides
|
|
659
344
|
|
|
660
|
-
|
|
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/
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
682
|
-
import { createClient } from '@exisjs/fetch'
|
|
683
|
-
import type { AppRouter } from '../server/.exis/types'
|
|
365
|
+
## Dependency Resolution in Functional Routes
|
|
684
366
|
|
|
685
|
-
|
|
686
|
-
baseUrl: 'http://localhost:4000',
|
|
687
|
-
})
|
|
367
|
+
Functional routes can resolve dependencies directly from the IoC container using `ctx.resolve()`:
|
|
688
368
|
|
|
689
|
-
|
|
690
|
-
|
|
369
|
+
```typescript filename="src/http/users/route.ts"
|
|
370
|
+
import { controller, route } from 'exisjs/router'
|
|
371
|
+
import { UserService } from './service'
|
|
691
372
|
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
##
|
|
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.
|