exisjs 0.7.12 → 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/config/types.d.ts +2 -2
- package/dist/config/types.d.ts.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 +138 -61
- package/dist/docs/content/cli/commands.mdx +151 -128
- package/dist/docs/content/cli/overview.mdx +61 -47
- package/dist/docs/content/cli/usage.mdx +107 -30
- package/dist/docs/content/config.mdx +145 -158
- 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 +235 -598
- 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 -75
- 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 +275 -293
- 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/start-server.js +3 -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/server/app.d.ts +3 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +24 -8
- package/dist/server/app.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 +11 -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 +46 -14
- package/dist/docs/content/dataloader.mdx +0 -170
- package/dist/docs/reference/dataloader.mdx +0 -91
|
@@ -1,753 +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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
```typescript filename="src/http/health/route.ts"
|
|
93
|
-
import { controller, route } from 'exisjs/router'
|
|
12
|
+
---
|
|
94
13
|
|
|
95
|
-
|
|
96
|
-
check: route.get('/', {
|
|
97
|
-
handle() {
|
|
98
|
-
return { status: 'ok', timestamp: new Date().toISOString() }
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
```
|
|
14
|
+
## Routing Principles
|
|
103
15
|
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
106
19
|
|
|
20
|
+
---
|
|
107
21
|
|
|
108
|
-
|
|
109
|
-
import { Controller, Get } from 'exisjs/decorators'
|
|
22
|
+
## File-System Route Conventions
|
|
110
23
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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) |
|
|
119
32
|
|
|
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
89
|
|
|
251
|
-
|
|
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
|
-
|
|
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.
|
|
458
|
-
|
|
459
|
-
### Declarative File Uploads
|
|
231
|
+
---
|
|
460
232
|
|
|
461
|
-
|
|
233
|
+
## Handler Execution Context (`SuperContext`)
|
|
462
234
|
|
|
463
|
-
|
|
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
|
-
|
|
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 |
|
|
498
262
|
|
|
499
|
-
|
|
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
|
-
```
|
|
263
|
+
---
|
|
519
264
|
|
|
520
|
-
|
|
265
|
+
## Declarative Route Configuration Options
|
|
521
266
|
|
|
522
|
-
|
|
267
|
+
Each route handler accepts an options object supporting validation, security policies, file uploads, and timeouts:
|
|
523
268
|
|
|
524
|
-
|
|
525
|
-
|
|
269
|
+
### 1. Request Validation Schemas
|
|
270
|
+
|
|
271
|
+
```typescript filename="src/http/products/route.ts"
|
|
526
272
|
import { controller, route } from 'exisjs/router'
|
|
273
|
+
import { tex } from 'exisjs/validator'
|
|
527
274
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
async handle() {
|
|
532
|
-
// Intensive document rendering or AI workflow
|
|
533
|
-
const reportUrl = await buildReport()
|
|
534
|
-
return { success: true, reportUrl }
|
|
535
|
-
}
|
|
536
|
-
})
|
|
275
|
+
const ProductQuerySchema = tex.object({
|
|
276
|
+
page: tex.number({ coerce: true, default: 1 }),
|
|
277
|
+
limit: tex.number({ coerce: true, default: 20 }),
|
|
537
278
|
})
|
|
538
|
-
```
|
|
539
279
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
#### Functional Paradigm
|
|
545
|
-
```typescript filename="src/http/webhooks/stripe/route.ts"
|
|
546
|
-
import { controller, route } from 'exisjs/router'
|
|
280
|
+
const ProductBodySchema = tex.object({
|
|
281
|
+
name: tex.string({ min: 2 }),
|
|
282
|
+
price: tex.number({ min: 0 }),
|
|
283
|
+
})
|
|
547
284
|
|
|
548
285
|
export default controller({
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
286
|
+
create: route.post('/', {
|
|
287
|
+
query: ProductQuerySchema,
|
|
288
|
+
body: ProductBodySchema,
|
|
289
|
+
handle: async ({ query, body }) => {
|
|
290
|
+
return { page: query.page, product: body }
|
|
554
291
|
},
|
|
555
|
-
handle({ req }) {
|
|
556
|
-
return { received: true }
|
|
557
|
-
}
|
|
558
292
|
}),
|
|
559
|
-
|
|
560
|
-
// Disable CORS completely for internal endpoints
|
|
561
|
-
internalHealth: route.get('/health', {
|
|
562
|
-
cors: false,
|
|
563
|
-
handle() {
|
|
564
|
-
return { status: 'healthy' }
|
|
565
|
-
}
|
|
566
|
-
})
|
|
567
293
|
})
|
|
568
294
|
```
|
|
569
295
|
|
|
570
|
-
|
|
571
|
-
```typescript filename="src/http/webhooks/stripe/route.ts"
|
|
572
|
-
import { Controller, Post, Cors } from 'exisjs/decorators'
|
|
573
|
-
|
|
574
|
-
@Controller('/webhooks/stripe')
|
|
575
|
-
export default class StripeWebhookController {
|
|
576
|
-
@Post('/')
|
|
577
|
-
@Cors({ origin: 'https://hooks.stripe.com', methods: ['POST'] })
|
|
578
|
-
handleWebhook() {
|
|
579
|
-
return { received: true }
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
### Query Pagination Schemas
|
|
296
|
+
---
|
|
585
297
|
|
|
586
|
-
|
|
298
|
+
### 2. Multipart File Uploads
|
|
587
299
|
|
|
588
|
-
```typescript filename="src/http/
|
|
300
|
+
```typescript filename="src/http/upload/route.ts"
|
|
589
301
|
import { controller, route } from 'exisjs/router'
|
|
590
|
-
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
591
|
-
|
|
592
|
-
const ProductQuerySchema = tex.pagination({
|
|
593
|
-
defaultLimit: 20,
|
|
594
|
-
maxLimit: 100,
|
|
595
|
-
})
|
|
596
302
|
|
|
597
303
|
export default controller({
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
})
|
|
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
|
+
}),
|
|
613
318
|
})
|
|
614
319
|
```
|
|
615
320
|
|
|
616
|
-
|
|
321
|
+
---
|
|
617
322
|
|
|
618
|
-
|
|
323
|
+
### 3. Route-Specific Timeout Overrides
|
|
619
324
|
|
|
620
|
-
|
|
325
|
+
You can override default timeouts for long-running endpoints:
|
|
621
326
|
|
|
622
|
-
```typescript filename="src/http/
|
|
327
|
+
```typescript filename="src/http/reports/route.ts"
|
|
623
328
|
import { controller, route } from 'exisjs/router'
|
|
624
|
-
import { UsersService } from './service'
|
|
625
329
|
|
|
626
330
|
export default controller({
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
})
|
|
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
|
+
}),
|
|
636
338
|
})
|
|
637
339
|
```
|
|
638
340
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
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.
|
|
341
|
+
---
|
|
642
342
|
|
|
643
|
-
|
|
343
|
+
### 4. Route-Specific CORS Overrides
|
|
644
344
|
|
|
345
|
+
Route-level CORS configuration takes precedence over boundary and global settings:
|
|
645
346
|
|
|
646
|
-
```typescript filename="src/http/
|
|
647
|
-
import { controller, route
|
|
347
|
+
```typescript filename="src/http/webhooks/stripe/route.ts"
|
|
348
|
+
import { controller, route } from 'exisjs/router'
|
|
648
349
|
|
|
649
350
|
export default controller({
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
// Run background tasks after sending response to client
|
|
660
|
-
after(async () => {
|
|
661
|
-
await sendNotificationEmail(userId)
|
|
662
|
-
})
|
|
663
|
-
|
|
664
|
-
res.status(201)
|
|
665
|
-
return { success: true }
|
|
666
|
-
}
|
|
667
|
-
})
|
|
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
|
+
}),
|
|
668
360
|
})
|
|
669
361
|
```
|
|
670
362
|
|
|
671
|
-
|
|
672
|
-
#### Class-Based (OOP) Paradigm
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
```typescript filename="src/http/users/route.ts"
|
|
676
|
-
import { Controller, Post } from 'exisjs/decorators'
|
|
677
|
-
import { getContext, getRequest, getResponse, after } from 'exisjs/router'
|
|
678
|
-
|
|
679
|
-
@Controller()
|
|
680
|
-
export default class OrdersController {
|
|
681
|
-
@Post('/')
|
|
682
|
-
async create() {
|
|
683
|
-
// You can still use the Context API seamlessly in OOP classes!
|
|
684
|
-
const req = getRequest()
|
|
685
|
-
const res = getResponse()
|
|
686
|
-
const { userId } = getContext<{ userId: string }>()
|
|
687
|
-
|
|
688
|
-
after(async () => {
|
|
689
|
-
await sendNotificationEmail(userId)
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
res.status(201)
|
|
693
|
-
return { success: true }
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
## Type-Safe Client Route Manifest Contract (`AppRouter`)
|
|
363
|
+
---
|
|
701
364
|
|
|
702
|
-
|
|
365
|
+
## Dependency Resolution in Functional Routes
|
|
703
366
|
|
|
704
|
-
|
|
705
|
-
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:
|
|
367
|
+
Functional routes can resolve dependencies directly from the IoC container using `ctx.resolve()`:
|
|
706
368
|
|
|
707
369
|
```typescript filename="src/http/users/route.ts"
|
|
708
370
|
import { controller, route } from 'exisjs/router'
|
|
709
|
-
import {
|
|
371
|
+
import { UserService } from './service'
|
|
710
372
|
|
|
711
373
|
export default controller({
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
})
|
|
719
|
-
})
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
### Consuming `AppRouter` with `@exisjs/fetch`
|
|
723
|
-
You can import `AppRouter` in your frontend or external client applications for zero-cost end-to-end type safety and autocomplete:
|
|
724
|
-
|
|
725
|
-
```typescript filename="src/client/api.ts"
|
|
726
|
-
import { createClient } from '@exisjs/fetch'
|
|
727
|
-
import type { AppRouter } from '../server/.exis/types'
|
|
728
|
-
|
|
729
|
-
const api = createClient<AppRouter>({
|
|
730
|
-
baseUrl: 'http://localhost:4000',
|
|
374
|
+
list: route.get('/', {
|
|
375
|
+
handle: async ({ resolve }) => {
|
|
376
|
+
const userService = resolve(UserService)
|
|
377
|
+
return userService.findAll()
|
|
378
|
+
},
|
|
379
|
+
}),
|
|
731
380
|
})
|
|
732
|
-
|
|
733
|
-
// 1. Hierarchical dot-notation RPC call:
|
|
734
|
-
const user = await api.users('123').getUser.get()
|
|
735
|
-
|
|
736
|
-
// 2. Direct path index call:
|
|
737
|
-
const userByPath = await api['/users/:id'].getUser.get({ params: { id: '123' } })
|
|
738
381
|
```
|
|
739
382
|
|
|
740
|
-
### Dynamic Dev Watcher Sync
|
|
741
|
-
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.
|
|
742
|
-
|
|
743
383
|
---
|
|
744
384
|
|
|
745
|
-
##
|
|
746
|
-
|
|
747
|
-
While putting all logic in `route.ts` is fine for small apps, ExisJS strongly encourages separating concerns alongside your route definition:
|
|
748
|
-
|
|
749
|
-
- **`schema.ts`**: Defines expected input validation constraints.
|
|
750
|
-
- **`service.ts`**: Pure functions that interact with your database.
|
|
751
|
-
- **`controller.ts`**: Handles business orchestration.
|
|
752
|
-
- **`route.ts`**: Wires it to the HTTP network.
|
|
385
|
+
## Next Steps
|
|
753
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.
|