exisjs 0.7.12 → 0.7.14
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/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +3 -1
- package/dist/cli/commands/run.js.map +1 -1
- 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/ARCHITECTURE.md +130 -78
- 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 +22 -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/fs-scanner.d.ts.map +1 -1
- package/dist/router/scanner/fs-scanner.js +22 -0
- package/dist/router/scanner/fs-scanner.js.map +1 -1
- package/dist/router/types.d.ts +6 -4
- package/dist/router/types.d.ts.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/bootstrapper.d.ts.map +1 -1
- package/dist/server/bootstrapper.js +19 -0
- package/dist/server/bootstrapper.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/server/response.d.ts +10 -5
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +65 -18
- package/dist/server/response.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 +19 -4
- 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,183 +1,134 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
title: 'Class-Based Controllers'
|
|
3
|
+
description: 'Defining Object-Oriented controllers with TypeScript decorators, route methods, parameter injection, pipes, and inheritance.'
|
|
4
|
+
nextUrl: '/docs/requests'
|
|
5
|
+
nextTitle: 'Request Object'
|
|
4
6
|
---
|
|
5
7
|
|
|
6
|
-
# Controllers
|
|
8
|
+
# Class-Based Controllers
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
ExisJS provides an Object-Oriented Programming (OOP) paradigm for developers who prefer class-based architecture, TypeScript decorators, and constructor-based Dependency Injection.
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
While ExisJS natively supports Functional Routing, developers coming from enterprise backgrounds often prefer Object-Oriented patterns. To create a basic OOP controller, we use classes and decorators. Decorators link classes with the necessary metadata, allowing ExisJS to automatically route requests.
|
|
12
|
+
---
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Defining a Controller
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
To declare a class-based controller, decorate a class with `@Controller()` and export it as default from a `route.ts` or `<name>.route.ts` file located inside `src/http/`:
|
|
17
17
|
|
|
18
18
|
```typescript filename="src/http/users/route.ts"
|
|
19
|
-
import { Controller, Get } from 'exisjs/decorators'
|
|
19
|
+
import { Controller, Get, Post, Body, ApiOperation } from 'exisjs/decorators'
|
|
20
20
|
|
|
21
|
-
@Controller(
|
|
21
|
+
@Controller()
|
|
22
22
|
export default class UsersController {
|
|
23
|
-
@Get()
|
|
24
|
-
|
|
23
|
+
@Get('/')
|
|
24
|
+
@ApiOperation({ summary: 'List all users' })
|
|
25
|
+
async listUsers() {
|
|
25
26
|
return [
|
|
26
27
|
{ id: 1, name: 'Alice' },
|
|
27
|
-
{ id: 2, name: 'Bob' }
|
|
28
|
+
{ id: 2, name: 'Bob' },
|
|
28
29
|
]
|
|
29
30
|
}
|
|
31
|
+
|
|
32
|
+
@Post('/')
|
|
33
|
+
@ApiOperation({ summary: 'Create user' })
|
|
34
|
+
async createUser(@Body() body: { name: string; email: string }) {
|
|
35
|
+
return { id: 3, ...body }
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
```
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
> [!NOTE]
|
|
41
|
+
> Route paths defined on `@Controller('/prefix')` or HTTP method decorators (`@Get('/subpath')`) are combined relative to the directory location inside `src/http/`.
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
---
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
## HTTP Method Decorators
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
import { Controller, Get, Req } from 'exisjs/decorators'
|
|
41
|
-
import type { Request } from 'exisjs/router'
|
|
47
|
+
ExisJS exposes decorators for all standard HTTP verbs:
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
| Decorator | HTTP Method | Example |
|
|
50
|
+
| :--- | :--- | :--- |
|
|
51
|
+
| `@Get(path?, schema?)` | `GET` | `@Get('/')` |
|
|
52
|
+
| `@Post(path?, schema?)` | `POST` | `@Post('/')` |
|
|
53
|
+
| `@Put(path?, schema?)` | `PUT` | `@Put('/:id')` |
|
|
54
|
+
| `@Patch(path?, schema?)` | `PATCH` | `@Patch('/:id')` |
|
|
55
|
+
| `@Delete(path?, schema?)` | `DELETE` | `@Delete('/:id')` |
|
|
56
|
+
| `@Options(path?, schema?)` | `OPTIONS` | `@Options('/')` |
|
|
57
|
+
| `@Head(path?, schema?)` | `HEAD` | `@Head('/')` |
|
|
58
|
+
| `@Connect(path?, schema?)` | `CONNECT` | `@Connect('/')` |
|
|
59
|
+
| `@Trace(path?, schema?)` | `TRACE` | `@Trace('/')` |
|
|
60
|
+
| `@Query(path?, schema?)` | `QUERY` | `@Query('/search')` |
|
|
61
|
+
| `@All(path?, schema?)` | Matches any method | `@All('*')` |
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<td className="px-4 py-3 text-muted-foreground">URL Query Parameters</td>
|
|
83
|
-
</tr>
|
|
84
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
85
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Headers(name?)</td>
|
|
86
|
-
<td className="px-4 py-3 text-muted-foreground">Specific HTTP Header</td>
|
|
87
|
-
</tr>
|
|
88
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
89
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>HostParam(name?)</td>
|
|
90
|
-
<td className="px-4 py-3 text-muted-foreground">Subdomain / Host route parameter</td>
|
|
91
|
-
</tr>
|
|
92
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
93
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Session()</td>
|
|
94
|
-
<td className="px-4 py-3 text-muted-foreground">The request session object</td>
|
|
95
|
-
</tr>
|
|
96
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
97
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Next()</td>
|
|
98
|
-
<td className="px-4 py-3 text-muted-foreground">Next middleware function in pipeline</td>
|
|
99
|
-
</tr>
|
|
100
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
101
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Ip()</td>
|
|
102
|
-
<td className="px-4 py-3 text-muted-foreground">The client's IP address</td>
|
|
103
|
-
</tr>
|
|
104
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
105
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Cookies(name?) / <span className="text-primary">@</span>Cookie(name?)</td>
|
|
106
|
-
<td className="px-4 py-3 text-muted-foreground">Incoming parsed cookies or specific cookie value</td>
|
|
107
|
-
</tr>
|
|
108
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
109
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>State(key?)</td>
|
|
110
|
-
<td className="px-4 py-3 text-muted-foreground">Request state from execution context or middleware</td>
|
|
111
|
-
</tr>
|
|
112
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
113
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>AppCtx()</td>
|
|
114
|
-
<td className="px-4 py-3 text-muted-foreground">The root ExisJS Application instance</td>
|
|
115
|
-
</tr>
|
|
116
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
117
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Fields(name?)</td>
|
|
118
|
-
<td className="px-4 py-3 text-muted-foreground">Non-file multipart form fields</td>
|
|
119
|
-
</tr>
|
|
120
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
121
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>UploadedFile()</td>
|
|
122
|
-
<td className="px-4 py-3 text-muted-foreground">A single uploaded file (multipart/form-data)</td>
|
|
123
|
-
</tr>
|
|
124
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
125
|
-
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>UploadedFiles()</td>
|
|
126
|
-
<td className="px-4 py-3 text-muted-foreground">Array or dictionary of uploaded files</td>
|
|
127
|
-
</tr>
|
|
128
|
-
</tbody>
|
|
129
|
-
</table>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
## Payloads & Validation
|
|
133
|
-
|
|
134
|
-
For POST, PUT, and PATCH endpoints, you can validate and read incoming payloads using either method-level schemas or parameter-level `@Body(Schema)` decorators:
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Parameter Decorators
|
|
66
|
+
|
|
67
|
+
ExisJS provides dedicated parameter decorators to extract and inject request data directly into controller method arguments:
|
|
68
|
+
|
|
69
|
+
| Decorator | Source / Injected Value | Example |
|
|
70
|
+
| :--- | :--- | :--- |
|
|
71
|
+
| `@Req()` | The raw `ExisRequest` instance | `getUser(@Req() req: Request)` |
|
|
72
|
+
| `@Res(options?)` | The `ExisResponse` instance | `send(@Res() res: Response)` |
|
|
73
|
+
| `@Param(name?)` | Route path parameter (`req.params`) | `getById(@Param('id') id: string)` |
|
|
74
|
+
| `@Body(schemaOrPipe?)` | Parsed request body payload (`req.body`) | `create(@Body() body: CreateDto)` |
|
|
75
|
+
| `@Query(nameOrPipe?)` / `@QueryParam(name?)` | URL query parameter (`req.query`) | `search(@Query('q') q: string)` |
|
|
76
|
+
| `@Headers(name?)` | Request headers (`req.headers`) | `auth(@Headers('authorization') token: string)` |
|
|
77
|
+
| `@HostParam(name?)` | Subdomain or host parameter | `handleHost(@HostParam('tenant') tenant: string)` |
|
|
78
|
+
| `@Cookies(name?)` / `@Cookie(name?)` | Parsed request cookies | `session(@Cookie('sid') sid: string)` |
|
|
79
|
+
| `@Ip()` | Client IP address (`req.ip`) | `audit(@Ip() clientIp: string)` |
|
|
80
|
+
| `@Session()` | Active session state (`req.session`) | `profile(@Session() session: any)` |
|
|
81
|
+
| `@State(key?)` | AsyncLocalStorage context state | `getState(@State('tenantId') id: string)` |
|
|
82
|
+
| `@AppCtx()` | Root ExisJS Application instance | `boot(@AppCtx() app: App)` |
|
|
83
|
+
| `@UploadedFile()` | Single uploaded file from multipart form | `upload(@UploadedFile() file: ExisFile)` |
|
|
84
|
+
| `@UploadedFiles()` | Array or map of uploaded files | `upload(@UploadedFiles() files: ExisFile[])` |
|
|
85
|
+
| `@Next()` | Next middleware function | `passthrough(@Next() next: NextFunction)` |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Request Validation with `tex` Schemas
|
|
90
|
+
|
|
91
|
+
You can validate incoming request bodies and parameters by passing schemas to method decorators or directly to parameter decorators:
|
|
135
92
|
|
|
136
93
|
```typescript filename="src/http/users/route.ts"
|
|
137
94
|
import { Controller, Post, Body, HttpCode } from 'exisjs/decorators'
|
|
138
|
-
import { tex } from 'exisjs/validator'
|
|
139
|
-
import type { ResolveSchema } from 'exisjs/validator'
|
|
95
|
+
import { tex, type ResolveSchema } from 'exisjs/validator'
|
|
140
96
|
|
|
141
|
-
export const
|
|
97
|
+
export const createUserSchema = tex.object({
|
|
142
98
|
name: tex.string({ min: 2 }),
|
|
143
99
|
email: tex.email(),
|
|
144
|
-
role: tex.enum(['admin', '
|
|
100
|
+
role: tex.enum(['admin', 'member'], { default: 'member' }),
|
|
145
101
|
})
|
|
146
102
|
|
|
147
|
-
export type CreateUserDto = ResolveSchema<typeof
|
|
103
|
+
export type CreateUserDto = ResolveSchema<typeof createUserSchema>
|
|
148
104
|
|
|
149
|
-
@Controller(
|
|
105
|
+
@Controller()
|
|
150
106
|
export default class UsersController {
|
|
151
|
-
|
|
152
|
-
@Post('/')
|
|
107
|
+
@Post('/', { body: createUserSchema })
|
|
153
108
|
@HttpCode(201)
|
|
154
|
-
async create(@Body(
|
|
155
|
-
return { id:
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// 2. Or method-level schema object
|
|
159
|
-
@Post('/raw', { body: CreateUserSchema })
|
|
160
|
-
async createWithMethodSchema(@Body() dto: CreateUserDto) {
|
|
161
|
-
return { id: 4, ...dto }
|
|
109
|
+
async create(@Body() dto: CreateUserDto) {
|
|
110
|
+
return { id: Date.now(), ...dto }
|
|
162
111
|
}
|
|
163
112
|
}
|
|
164
113
|
```
|
|
165
114
|
|
|
115
|
+
---
|
|
116
|
+
|
|
166
117
|
## Dependency Injection in Controllers
|
|
167
118
|
|
|
168
|
-
Controllers
|
|
119
|
+
Controllers support dependency injection through constructor injection or property-level injection via `inject()`:
|
|
169
120
|
|
|
170
121
|
```typescript filename="src/http/users/route.ts"
|
|
171
122
|
import { Controller, Get, Param } from 'exisjs/decorators'
|
|
172
123
|
import { inject, Inject, Optional } from 'exisjs/di'
|
|
173
|
-
import {
|
|
124
|
+
import { UserService } from './service'
|
|
174
125
|
|
|
175
|
-
@Controller(
|
|
126
|
+
@Controller()
|
|
176
127
|
export default class UsersController {
|
|
177
|
-
//
|
|
178
|
-
private
|
|
128
|
+
// 1. Property-level injection
|
|
129
|
+
private userService = inject(UserService)
|
|
179
130
|
|
|
180
|
-
//
|
|
131
|
+
// 2. Constructor injection with custom tokens
|
|
181
132
|
constructor(
|
|
182
133
|
@Inject('DATABASE_URL') private dbUrl: string,
|
|
183
134
|
@Optional() @Inject('METRICS') private metrics?: any
|
|
@@ -185,404 +136,182 @@ export default class UsersController {
|
|
|
185
136
|
|
|
186
137
|
@Get('/:id')
|
|
187
138
|
async getUser(@Param('id') id: string) {
|
|
188
|
-
return this.
|
|
139
|
+
return this.userService.getUserById(id)
|
|
189
140
|
}
|
|
190
141
|
}
|
|
191
142
|
```
|
|
192
143
|
|
|
193
|
-
|
|
144
|
+
---
|
|
194
145
|
|
|
195
|
-
|
|
146
|
+
## Parameter Transformation with Built-In Pipes
|
|
196
147
|
|
|
197
|
-
|
|
198
|
-
import { Controller, Post, UseUpload, UploadedFile, Body } from 'exisjs/decorators'
|
|
199
|
-
import type { UploadedFile as IUploadedFile } from 'exisjs/router'
|
|
148
|
+
Pipes transform and validate incoming request data before your controller method executes. If validation fails, pipes throw an appropriate `HttpError` (e.g., `400 Bad Request`):
|
|
200
149
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
size: file.size,
|
|
217
|
-
caption: body.caption,
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Streaming & Downloads
|
|
224
|
-
|
|
225
|
-
OOP controllers support all streaming features including Node.js `Readable` streams, Web `ReadableStream`, async generators, and file downloads:
|
|
150
|
+
```typescript filename="src/http/items/route.ts"
|
|
151
|
+
import {
|
|
152
|
+
Controller,
|
|
153
|
+
Get,
|
|
154
|
+
Post,
|
|
155
|
+
Param,
|
|
156
|
+
Query,
|
|
157
|
+
Body,
|
|
158
|
+
UsePipes,
|
|
159
|
+
ParseIntPipe,
|
|
160
|
+
ParseBoolPipe,
|
|
161
|
+
ParseUUIDPipe,
|
|
162
|
+
ParseArrayPipe,
|
|
163
|
+
ValidationPipe,
|
|
164
|
+
} from 'exisjs/decorators'
|
|
226
165
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
166
|
+
@Controller('/items')
|
|
167
|
+
export default class ItemsController {
|
|
168
|
+
// Parses string route parameter to integer
|
|
169
|
+
@Get('/:id')
|
|
170
|
+
async getItem(@Param('id', ParseIntPipe) id: number) {
|
|
171
|
+
return { id, type: typeof id }
|
|
172
|
+
}
|
|
231
173
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
174
|
+
// Validates boolean query string and comma-separated integer array
|
|
175
|
+
@Get('/')
|
|
176
|
+
async listItems(
|
|
177
|
+
@Query('active', ParseBoolPipe) active: boolean,
|
|
178
|
+
@Query('ids', new ParseArrayPipe({ items: ParseIntPipe })) ids: number[]
|
|
179
|
+
) {
|
|
180
|
+
return { active, ids }
|
|
238
181
|
}
|
|
239
182
|
|
|
240
|
-
//
|
|
241
|
-
@Get('/
|
|
242
|
-
|
|
243
|
-
return
|
|
183
|
+
// Validates standard UUID format
|
|
184
|
+
@Get('/by-uuid/:uuid')
|
|
185
|
+
async getByUUID(@Param('uuid', ParseUUIDPipe) uuid: string) {
|
|
186
|
+
return { uuid }
|
|
244
187
|
}
|
|
245
188
|
|
|
246
|
-
//
|
|
247
|
-
@
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
189
|
+
// Method-level validation pipe
|
|
190
|
+
@Post('/')
|
|
191
|
+
@UsePipes(new ValidationPipe({ transform: true }))
|
|
192
|
+
async createItem(@Body() body: any) {
|
|
193
|
+
return { created: true, body }
|
|
251
194
|
}
|
|
252
195
|
}
|
|
253
196
|
```
|
|
254
197
|
|
|
255
|
-
|
|
198
|
+
---
|
|
256
199
|
|
|
257
|
-
|
|
200
|
+
## Status Codes, Headers & Redirects
|
|
258
201
|
|
|
259
|
-
|
|
260
|
-
import { Controller, Post, HttpCode, Header } from 'exisjs/decorators'
|
|
202
|
+
You can customize response status codes, HTTP headers, and URL redirects declaratively using decorators:
|
|
261
203
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
204
|
+
```typescript filename="src/http/orders/route.ts"
|
|
205
|
+
import { Controller, Post, Get, HttpCode, Header, Redirect } from 'exisjs/decorators'
|
|
206
|
+
|
|
207
|
+
@Controller()
|
|
208
|
+
export default class OrdersController {
|
|
209
|
+
@Post('/')
|
|
265
210
|
@HttpCode(204)
|
|
266
211
|
@Header('Cache-Control', 'no-store')
|
|
267
|
-
|
|
268
|
-
return null
|
|
212
|
+
async createOrder() {
|
|
213
|
+
return null
|
|
269
214
|
}
|
|
215
|
+
|
|
216
|
+
@Get('/legacy-checkout')
|
|
217
|
+
@Redirect('/checkout', 301)
|
|
218
|
+
legacyCheckout() {}
|
|
270
219
|
}
|
|
271
220
|
```
|
|
272
221
|
|
|
273
|
-
|
|
222
|
+
---
|
|
274
223
|
|
|
275
|
-
|
|
224
|
+
## Multipart File Uploads
|
|
276
225
|
|
|
277
|
-
|
|
226
|
+
Handle file uploads using the `@Upload` (or `@UseUpload`) decorator alongside `@UploadedFile()` and `@UploadedFiles()`:
|
|
278
227
|
|
|
279
|
-
```typescript filename="src/http/
|
|
280
|
-
import {
|
|
228
|
+
```typescript filename="src/http/avatar/route.ts"
|
|
229
|
+
import { Controller, Post, Upload, UploadedFile, Body } from 'exisjs/decorators'
|
|
230
|
+
import type { ExisFile } from 'exisjs/router'
|
|
281
231
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
}),
|
|
290
|
-
// 2. Redirect
|
|
291
|
-
redirect: route.get('/redirect', {
|
|
292
|
-
handle({ res }) {
|
|
293
|
-
res.redirect('https://google.com')
|
|
294
|
-
}
|
|
295
|
-
}),
|
|
296
|
-
// 3. Passthrough Response
|
|
297
|
-
manual: route.get('/manual', {
|
|
298
|
-
handle({ res }) {
|
|
299
|
-
res.setHeader('X-Custom', 'true')
|
|
300
|
-
return { msg: 'This is auto-serialized with custom headers!' }
|
|
301
|
-
}
|
|
232
|
+
@Controller()
|
|
233
|
+
export default class AvatarController {
|
|
234
|
+
@Post('/')
|
|
235
|
+
@Upload('avatar', {
|
|
236
|
+
maxBytes: 5 * 1024 * 1024,
|
|
237
|
+
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
302
238
|
})
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
@Controller({ host: ':sub.exis.local' })
|
|
314
|
-
export class SubdomainController {
|
|
315
|
-
@Get('/ping')
|
|
316
|
-
ping(@HostParam('sub') sub: string) {
|
|
317
|
-
return { subdomain: sub }
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
@Controller('users')
|
|
322
|
-
export class AdvancedController {
|
|
323
|
-
// 2. Redirect
|
|
324
|
-
@Get('/redirect')
|
|
325
|
-
@Redirect('https://google.com')
|
|
326
|
-
redirect() {}
|
|
327
|
-
|
|
328
|
-
// 3. Passthrough Response
|
|
329
|
-
@Get('/manual')
|
|
330
|
-
manual(@Res({ passthrough: true }) res: Response) {
|
|
331
|
-
res.setHeader('X-Custom', 'true')
|
|
332
|
-
return { msg: 'This is auto-serialized with custom headers!' }
|
|
239
|
+
async uploadAvatar(
|
|
240
|
+
@UploadedFile() file: ExisFile,
|
|
241
|
+
@Body('caption') caption?: string
|
|
242
|
+
) {
|
|
243
|
+
await file.saveToDisk('./uploads')
|
|
244
|
+
return {
|
|
245
|
+
filename: file.filename,
|
|
246
|
+
size: file.size,
|
|
247
|
+
caption,
|
|
248
|
+
}
|
|
333
249
|
}
|
|
334
250
|
}
|
|
335
251
|
```
|
|
336
252
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
Unlike frameworks where controllers must be registered manually in a module file, ExisJS uses Folder-Based Routing. To register a controller, you export default the class inside a `route.ts` file.
|
|
340
|
-
|
|
341
|
-
If you need low-level control over the response, you can inject the native `@Res()` object.
|
|
342
|
-
|
|
343
|
-
```typescript filename="src/http/users/route.ts"
|
|
344
|
-
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
345
|
-
import type { Response } from 'exisjs/router'
|
|
253
|
+
---
|
|
346
254
|
|
|
347
|
-
|
|
348
|
-
export default class UsersController {
|
|
349
|
-
@Get()
|
|
350
|
-
findAll(@Res() res: Response) {
|
|
351
|
-
res.status(200).json({ data: [] })
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
```
|
|
255
|
+
## Controller Inheritance
|
|
355
256
|
|
|
356
|
-
|
|
257
|
+
ExisJS supports Object-Oriented class inheritance. Subclasses inherit all route handlers, parameter decorators, validation rules, and guards from base controllers:
|
|
357
258
|
|
|
358
|
-
|
|
259
|
+
### Base Generic Controller
|
|
359
260
|
|
|
360
261
|
```typescript filename="src/http/base-crud.controller.ts"
|
|
361
|
-
import { Get,
|
|
262
|
+
import { Get, Delete, Param } from 'exisjs/decorators'
|
|
362
263
|
|
|
363
264
|
export abstract class BaseCrudController<T> {
|
|
364
|
-
protected abstract
|
|
265
|
+
protected abstract service: {
|
|
266
|
+
findAll: () => Promise<T[]>
|
|
267
|
+
findById: (id: string) => Promise<T | null>
|
|
268
|
+
remove: (id: string) => Promise<boolean>
|
|
269
|
+
}
|
|
365
270
|
|
|
366
271
|
@Get('/')
|
|
367
272
|
async findAll(): Promise<T[]> {
|
|
368
|
-
return
|
|
273
|
+
return this.service.findAll()
|
|
369
274
|
}
|
|
370
275
|
|
|
371
276
|
@Get('/:id')
|
|
372
|
-
async findById(@Param('id') id: string): Promise<T> {
|
|
373
|
-
return
|
|
277
|
+
async findById(@Param('id') id: string): Promise<T | null> {
|
|
278
|
+
return this.service.findById(id)
|
|
374
279
|
}
|
|
375
280
|
|
|
376
281
|
@Delete('/:id')
|
|
377
282
|
async remove(@Param('id') id: string): Promise<{ deleted: boolean }> {
|
|
378
|
-
|
|
283
|
+
const deleted = await this.service.remove(id)
|
|
284
|
+
return { deleted }
|
|
379
285
|
}
|
|
380
286
|
}
|
|
381
287
|
```
|
|
382
288
|
|
|
383
|
-
|
|
289
|
+
### Concrete Controller Subclass
|
|
384
290
|
|
|
385
291
|
```typescript filename="src/http/users/route.ts"
|
|
386
|
-
import { Controller, Post, Body
|
|
292
|
+
import { Controller, Post, Body } from 'exisjs/decorators'
|
|
387
293
|
import { BaseCrudController } from '../base-crud.controller'
|
|
294
|
+
import { UserService, type User } from './service'
|
|
388
295
|
|
|
389
|
-
|
|
390
|
-
id: string
|
|
391
|
-
name: string
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
@Controller('/users')
|
|
296
|
+
@Controller()
|
|
395
297
|
export default class UsersController extends BaseCrudController<User> {
|
|
396
|
-
protected
|
|
298
|
+
constructor(protected readonly service: UserService) {
|
|
299
|
+
super()
|
|
300
|
+
}
|
|
397
301
|
|
|
398
|
-
//
|
|
302
|
+
// Add specialized endpoint
|
|
399
303
|
@Post('/')
|
|
400
304
|
async create(@Body() user: User) {
|
|
401
|
-
return
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Subclass can override inherited remove handler
|
|
405
|
-
override async remove(@Param('id') id: string) {
|
|
406
|
-
return { deleted: true, id, customAudit: true }
|
|
305
|
+
return this.service.create(user)
|
|
407
306
|
}
|
|
408
307
|
}
|
|
409
308
|
```
|
|
410
309
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
In the functional paradigm, inheritance is achieved via `controller.extend(base, overrides)`. This inherits all route handlers, lifecycle hooks, and middlewares from a base controller factory and allows overriding endpoints or adding new routes with full type inference:
|
|
414
|
-
|
|
415
|
-
```typescript filename="src/http/products/route.ts"
|
|
416
|
-
import { controller, route } from 'exisjs/router'
|
|
417
|
-
|
|
418
|
-
// 1. Generic functional base CRUD controller
|
|
419
|
-
function createBaseCrud(resourceName: string) {
|
|
420
|
-
return controller({
|
|
421
|
-
list: route.get('/', {
|
|
422
|
-
async handle() {
|
|
423
|
-
return { resource: resourceName, items: [] }
|
|
424
|
-
}
|
|
425
|
-
}),
|
|
426
|
-
getById: route.get('/:id', {
|
|
427
|
-
async handle({ params }) {
|
|
428
|
-
return { resource: resourceName, id: params.id }
|
|
429
|
-
}
|
|
430
|
-
}),
|
|
431
|
-
remove: route.delete('/:id', {
|
|
432
|
-
async handle({ params }) {
|
|
433
|
-
return { deleted: true, id: params.id }
|
|
434
|
-
}
|
|
435
|
-
})
|
|
436
|
-
})
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// 2. Extend base controller with overrides and additional routes
|
|
440
|
-
export default controller.extend(createBaseCrud('products'), {
|
|
441
|
-
// Additional specialized route
|
|
442
|
-
featured: route.get('/featured', {
|
|
443
|
-
async handle() {
|
|
444
|
-
return { featured: ['Product 1', 'Product 2'] }
|
|
445
|
-
}
|
|
446
|
-
}),
|
|
447
|
-
|
|
448
|
-
// Override base endpoint
|
|
449
|
-
getById: route.get('/:id', {
|
|
450
|
-
async handle({ params }) {
|
|
451
|
-
return { customProduct: true, id: params.id }
|
|
452
|
-
}
|
|
453
|
-
})
|
|
454
|
-
})
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
## Functional Controller Composition (`controller.merge`)
|
|
458
|
-
|
|
459
|
-
When a single `route.ts` manages a feature area with multiple concerns (for example, auth flows containing credentials, sessions, and OAuth), `controller.merge(...controllers)` allows splitting logic into modular controller objects and merging them into a single default export:
|
|
460
|
-
|
|
461
|
-
```typescript filename="src/http/auth/route.ts"
|
|
462
|
-
import { controller, route } from 'exisjs/router'
|
|
463
|
-
|
|
464
|
-
// 1. Modular controller for password authentication
|
|
465
|
-
const passwordAuth = controller({
|
|
466
|
-
login: route.post('/login', {
|
|
467
|
-
async handle({ body }) {
|
|
468
|
-
return { token: 'jwt-auth-token' }
|
|
469
|
-
}
|
|
470
|
-
}),
|
|
471
|
-
logout: route.post('/logout', {
|
|
472
|
-
async handle() {
|
|
473
|
-
return { success: true }
|
|
474
|
-
}
|
|
475
|
-
})
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
// 2. Modular controller for OAuth providers
|
|
479
|
-
const oauthProviders = controller({
|
|
480
|
-
google: route.get('/google', {
|
|
481
|
-
async handle() {
|
|
482
|
-
return { redirectUrl: 'https://accounts.google.com/o/oauth2/auth' }
|
|
483
|
-
}
|
|
484
|
-
}),
|
|
485
|
-
github: route.get('/github', {
|
|
486
|
-
async handle() {
|
|
487
|
-
return { redirectUrl: 'https://github.com/login/oauth/authorize' }
|
|
488
|
-
}
|
|
489
|
-
})
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
// 3. Merge both into the single exported controller for this folder
|
|
493
|
-
export default controller.merge(passwordAuth, oauthProviders)
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
## Built-in Transformation Pipes & `@UsePipes`
|
|
497
|
-
|
|
498
|
-
Pipes transform and validate incoming request data before it reaches the route handler. ExisJS provides built-in pipes for common primitives:
|
|
499
|
-
|
|
500
|
-
<div className="overflow-x-auto mb-6">
|
|
501
|
-
<table className="w-full text-sm text-left border border-border/50 rounded-lg overflow-hidden">
|
|
502
|
-
<thead className="bg-muted/50 text-foreground">
|
|
503
|
-
<tr>
|
|
504
|
-
<th className="px-4 py-3 border-b border-border/50 font-semibold w-1/3">Pipe</th>
|
|
505
|
-
<th className="px-4 py-3 border-b border-border/50 font-semibold">Description</th>
|
|
506
|
-
</tr>
|
|
507
|
-
</thead>
|
|
508
|
-
<tbody className="divide-y divide-border/50">
|
|
509
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
510
|
-
<td className="px-4 py-3 font-mono text-xs">ParseIntPipe</td>
|
|
511
|
-
<td className="px-4 py-3 text-muted-foreground">Parses string/number parameter into integer or throws 400 Bad Request</td>
|
|
512
|
-
</tr>
|
|
513
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
514
|
-
<td className="px-4 py-3 font-mono text-xs">ParseFloatPipe</td>
|
|
515
|
-
<td className="px-4 py-3 text-muted-foreground">Parses parameter into floating-point number</td>
|
|
516
|
-
</tr>
|
|
517
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
518
|
-
<td className="px-4 py-3 font-mono text-xs">ParseBoolPipe</td>
|
|
519
|
-
<td className="px-4 py-3 text-muted-foreground">Parses boolean strings (<code>'true'</code>, <code>'false'</code>, <code>1</code>, <code>0</code>) into native booleans</td>
|
|
520
|
-
</tr>
|
|
521
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
522
|
-
<td className="px-4 py-3 font-mono text-xs">ParseUUIDPipe</td>
|
|
523
|
-
<td className="px-4 py-3 text-muted-foreground">Validates UUID format (v3, v4, v5, or all)</td>
|
|
524
|
-
</tr>
|
|
525
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
526
|
-
<td className="px-4 py-3 font-mono text-xs">ParseArrayPipe</td>
|
|
527
|
-
<td className="px-4 py-3 text-muted-foreground">Splits comma-separated strings into arrays and applies item pipes</td>
|
|
528
|
-
</tr>
|
|
529
|
-
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
530
|
-
<td className="px-4 py-3 font-mono text-xs">ValidationPipe</td>
|
|
531
|
-
<td className="px-4 py-3 text-muted-foreground">Validates and transforms DTO classes or schema metatypes</td>
|
|
532
|
-
</tr>
|
|
533
|
-
</tbody>
|
|
534
|
-
</table>
|
|
535
|
-
</div>
|
|
536
|
-
|
|
537
|
-
```typescript filename="src/http/items/route.ts"
|
|
538
|
-
import {
|
|
539
|
-
Controller,
|
|
540
|
-
Get,
|
|
541
|
-
Post,
|
|
542
|
-
Param,
|
|
543
|
-
Query,
|
|
544
|
-
Body,
|
|
545
|
-
UsePipes,
|
|
546
|
-
ParseIntPipe,
|
|
547
|
-
ParseBoolPipe,
|
|
548
|
-
ParseUUIDPipe,
|
|
549
|
-
ParseArrayPipe,
|
|
550
|
-
ValidationPipe,
|
|
551
|
-
} from 'exisjs/decorators'
|
|
552
|
-
|
|
553
|
-
@Controller('/items')
|
|
554
|
-
export default class ItemsController {
|
|
555
|
-
// 1. Parameter-level pipe
|
|
556
|
-
@Get('/:id')
|
|
557
|
-
async getItem(@Param('id', ParseIntPipe) id: number) {
|
|
558
|
-
return { id, isNumber: typeof id === 'number' }
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// 2. Query boolean & array pipes
|
|
562
|
-
@Get('/')
|
|
563
|
-
async listItems(
|
|
564
|
-
@Query('active', ParseBoolPipe) active: boolean,
|
|
565
|
-
@Query('ids', new ParseArrayPipe({ items: ParseIntPipe })) ids: number[]
|
|
566
|
-
) {
|
|
567
|
-
return { active, ids }
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// 3. UUID validation pipe
|
|
571
|
-
@Get('/by-uuid/:uuid')
|
|
572
|
-
async getByUUID(@Param('uuid', ParseUUIDPipe) uuid: string) {
|
|
573
|
-
return { uuid }
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// 4. Method-level or Class-level @UsePipes
|
|
577
|
-
@Post('/')
|
|
578
|
-
@UsePipes(new ValidationPipe({ transform: true }))
|
|
579
|
-
async createItem(@Body() body: any) {
|
|
580
|
-
return { created: true, body }
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
```
|
|
310
|
+
---
|
|
584
311
|
|
|
585
|
-
|
|
586
|
-
In some frameworks, injecting the `@Res()` object disables auto-serialization, causing the request to hang if you forget to call `res.send()`. ExisJS handles this differently: if you return a value from your handler, ExisJS will still safely auto-serialize it to JSON even if you injected `@Res()` to set a custom header.
|
|
587
|
-
</Info>
|
|
312
|
+
## Next Steps
|
|
588
313
|
|
|
314
|
+
- **[Request Object](/docs/requests)**: Inspect headers, query strings, and custom context on `ExisRequest`.
|
|
315
|
+
- **[Response Helpers](/docs/responses)**: Response shaping, custom cookies, streaming, and Server-Sent Events.
|
|
316
|
+
- **[Providers & DI](/docs/providers)**: Deep dive into the IoC container and custom provider lifecycles.
|
|
317
|
+
- **[Boundaries](/docs/boundaries)**: Apply scoped middleware and guards to entire controller directories.
|