exisjs 0.7.11 → 0.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +195 -39
  2. package/dist/cli/commands/dev.d.ts +4 -0
  3. package/dist/cli/commands/dev.d.ts.map +1 -1
  4. package/dist/cli/commands/dev.js +136 -32
  5. package/dist/cli/commands/dev.js.map +1 -1
  6. package/dist/cli/commands/run.d.ts +7 -0
  7. package/dist/cli/commands/run.d.ts.map +1 -0
  8. package/dist/cli/commands/run.js +99 -0
  9. package/dist/cli/commands/run.js.map +1 -0
  10. package/dist/cli/index.js +29 -13
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/manifest.d.ts.map +1 -1
  13. package/dist/cli/manifest.js +18 -3
  14. package/dist/cli/manifest.js.map +1 -1
  15. package/dist/cli/routes.d.ts +6 -0
  16. package/dist/cli/routes.d.ts.map +1 -1
  17. package/dist/cli/routes.js +224 -135
  18. package/dist/cli/routes.js.map +1 -1
  19. package/dist/config/config.d.ts +2 -1
  20. package/dist/config/config.d.ts.map +1 -1
  21. package/dist/config/config.js.map +1 -1
  22. package/dist/config/types.d.ts +30 -9
  23. package/dist/config/types.d.ts.map +1 -1
  24. package/dist/database/lifecycle.d.ts +6 -1
  25. package/dist/database/lifecycle.d.ts.map +1 -1
  26. package/dist/database/lifecycle.js +15 -4
  27. package/dist/database/lifecycle.js.map +1 -1
  28. package/dist/decorators/params.d.ts +21 -0
  29. package/dist/decorators/params.d.ts.map +1 -1
  30. package/dist/decorators/params.js +21 -1
  31. package/dist/decorators/params.js.map +1 -1
  32. package/dist/di/container.d.ts +15 -6
  33. package/dist/di/container.d.ts.map +1 -1
  34. package/dist/di/container.js +26 -3
  35. package/dist/di/container.js.map +1 -1
  36. package/dist/di/index.d.ts +1 -1
  37. package/dist/di/index.d.ts.map +1 -1
  38. package/dist/di/index.js.map +1 -1
  39. package/dist/di/inject.d.ts +2 -1
  40. package/dist/di/inject.d.ts.map +1 -1
  41. package/dist/di/inject.js +2 -1
  42. package/dist/di/inject.js.map +1 -1
  43. package/dist/docs/BENCHMARKS.md +4 -4
  44. package/dist/docs/content/README.md +43 -44
  45. package/dist/docs/content/boundaries.mdx +141 -48
  46. package/dist/docs/content/cli/commands.mdx +161 -107
  47. package/dist/docs/content/cli/overview.mdx +61 -46
  48. package/dist/docs/content/cli/usage.mdx +107 -30
  49. package/dist/docs/content/config.mdx +145 -147
  50. package/dist/docs/content/controllers.mdx +193 -464
  51. package/dist/docs/content/cron.mdx +183 -69
  52. package/dist/docs/content/decorators.mdx +449 -154
  53. package/dist/docs/content/errors.mdx +202 -305
  54. package/dist/docs/content/exisjs-mcp.mdx +59 -49
  55. package/dist/docs/content/installation.mdx +243 -157
  56. package/dist/docs/content/introduction.mdx +220 -48
  57. package/dist/docs/content/middleware.mdx +171 -497
  58. package/dist/docs/content/modules.mdx +141 -83
  59. package/dist/docs/content/providers.mdx +216 -180
  60. package/dist/docs/content/requests.mdx +168 -488
  61. package/dist/docs/content/responses.mdx +200 -533
  62. package/dist/docs/content/routing.mdx +239 -558
  63. package/dist/docs/content/security.mdx +233 -57
  64. package/dist/docs/content/structure.mdx +151 -240
  65. package/dist/docs/content/swagger.mdx +192 -91
  66. package/dist/docs/content/testing.mdx +205 -74
  67. package/dist/docs/content/validation.mdx +479 -341
  68. package/dist/docs/reference/README.md +34 -2
  69. package/dist/docs/reference/config.mdx +277 -73
  70. package/dist/docs/reference/context.mdx +197 -106
  71. package/dist/docs/reference/cron.mdx +266 -152
  72. package/dist/docs/reference/database.mdx +135 -53
  73. package/dist/docs/reference/decorators.mdx +234 -116
  74. package/dist/docs/reference/di.mdx +146 -112
  75. package/dist/docs/reference/error.mdx +155 -178
  76. package/dist/docs/reference/exis.mdx +141 -70
  77. package/dist/docs/reference/logger.mdx +110 -126
  78. package/dist/docs/reference/middleware.mdx +235 -158
  79. package/dist/docs/reference/plugin.mdx +136 -64
  80. package/dist/docs/reference/router.mdx +277 -251
  81. package/dist/docs/reference/sanitize.mdx +91 -87
  82. package/dist/docs/reference/swagger.mdx +280 -100
  83. package/dist/docs/reference/testing.mdx +223 -89
  84. package/dist/docs/reference/validator.mdx +194 -206
  85. package/dist/lib/inspect-routes.d.ts +2 -0
  86. package/dist/lib/inspect-routes.d.ts.map +1 -0
  87. package/dist/lib/inspect-routes.js +148 -0
  88. package/dist/lib/inspect-routes.js.map +1 -0
  89. package/dist/lib/start-server.js +13 -8
  90. package/dist/lib/start-server.js.map +1 -1
  91. package/dist/module/module.d.ts +6 -3
  92. package/dist/module/module.d.ts.map +1 -1
  93. package/dist/module/module.js +17 -3
  94. package/dist/module/module.js.map +1 -1
  95. package/dist/router/controller-registrar.d.ts.map +1 -1
  96. package/dist/router/controller-registrar.js +26 -4
  97. package/dist/router/controller-registrar.js.map +1 -1
  98. package/dist/router/scanner/cron-mounter.d.ts.map +1 -1
  99. package/dist/router/scanner/cron-mounter.js +9 -2
  100. package/dist/router/scanner/cron-mounter.js.map +1 -1
  101. package/dist/server/app.d.ts +3 -1
  102. package/dist/server/app.d.ts.map +1 -1
  103. package/dist/server/app.js +29 -8
  104. package/dist/server/app.js.map +1 -1
  105. package/dist/server/define.d.ts.map +1 -1
  106. package/dist/server/define.js +5 -0
  107. package/dist/server/define.js.map +1 -1
  108. package/dist/server/request.d.ts +2 -0
  109. package/dist/server/request.d.ts.map +1 -1
  110. package/dist/server/request.js +11 -0
  111. package/dist/server/request.js.map +1 -1
  112. package/dist/swagger/generator.d.ts.map +1 -1
  113. package/dist/swagger/generator.js +130 -10
  114. package/dist/swagger/generator.js.map +1 -1
  115. package/dist/telemetry/index.d.ts +12 -0
  116. package/dist/telemetry/index.d.ts.map +1 -0
  117. package/dist/telemetry/index.js +121 -0
  118. package/dist/telemetry/index.js.map +1 -0
  119. package/dist/testing/client.d.ts +1 -0
  120. package/dist/testing/client.d.ts.map +1 -1
  121. package/dist/testing/client.js +4 -1
  122. package/dist/testing/client.js.map +1 -1
  123. package/dist/testing/index.d.ts.map +1 -1
  124. package/dist/testing/index.js +20 -4
  125. package/dist/testing/index.js.map +1 -1
  126. package/dist/validator/builder.d.ts +26 -1
  127. package/dist/validator/builder.d.ts.map +1 -1
  128. package/dist/validator/builder.js +31 -0
  129. package/dist/validator/builder.js.map +1 -1
  130. package/dist/validator/engine.d.ts +23 -0
  131. package/dist/validator/engine.d.ts.map +1 -1
  132. package/dist/validator/engine.js +228 -31
  133. package/dist/validator/engine.js.map +1 -1
  134. package/dist/validator/index.d.ts +8 -4
  135. package/dist/validator/index.d.ts.map +1 -1
  136. package/dist/validator/index.js +4 -1
  137. package/dist/validator/index.js.map +1 -1
  138. package/dist/validator/tex-types.d.ts +15 -0
  139. package/dist/validator/tex-types.d.ts.map +1 -1
  140. package/dist/validator/tex-types.js +102 -0
  141. package/dist/validator/tex-types.js.map +1 -1
  142. package/dist/validator/tex.d.ts +1 -1
  143. package/dist/validator/tex.d.ts.map +1 -1
  144. package/dist/validator/tex.js +3 -1
  145. package/dist/validator/tex.js.map +1 -1
  146. package/dist/validator/types.d.ts +1 -0
  147. package/dist/validator/types.d.ts.map +1 -1
  148. package/package.json +47 -14
  149. package/dist/docs/content/dataloader.mdx +0 -170
  150. package/dist/docs/reference/dataloader.mdx +0 -91
@@ -1,183 +1,134 @@
1
1
  ---
2
- nextUrl: "/docs/providers"
3
- nextTitle: "Providers"
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
- Controllers are responsible for handling incoming requests and sending responses back to the client.
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
- A controller's purpose is to handle specific requests for the application. The routing mechanism determines which controller will handle each request. Often, a controller has multiple routes, and each route can perform a different action.
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
- ## Routing & Prefixes
14
+ ## Defining a Controller
15
15
 
16
- In the following example, we'll use the `@Controller()` decorator, which is required to define a basic OOP controller. We'll specify an optional route path prefix of `users`. Using a path prefix helps group related routes together.
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('users')
21
+ @Controller()
22
22
  export default class UsersController {
23
- @Get()
24
- findAll(): any[] {
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
- The `@Get()` HTTP request method decorator placed before the `findAll()` method tells ExisJS to create a handler for GET requests. Because the controller has a prefix of `users`, this endpoint maps to `GET /users`. If we used `@Get('active')`, it would map to `GET /users/active`.
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
- ## Request Object
43
+ ---
36
44
 
37
- Handlers often need access to the client’s request details. You can access the request object by injecting it using the `@Req()` decorator.
45
+ ## HTTP Method Decorators
38
46
 
39
- ```typescript filename="src/http/users/route.ts"
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
- @Controller('users')
44
- export default class UsersController {
45
- @Get()
46
- findAll(@Req() request: Request): any[] {
47
- console.log(request.ip, request.headers)
48
- return []
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
- In most cases, you don't need to manually access the entire request object. Instead, you can use dedicated decorators provided out of the box:
54
-
55
- <div className="overflow-x-auto mb-6">
56
- <table className="w-full text-sm text-left border border-border/50 rounded-lg overflow-hidden">
57
- <thead className="bg-muted/50 text-foreground">
58
- <tr>
59
- <th className="px-4 py-3 border-b border-border/50 font-semibold w-1/3">Decorator</th>
60
- <th className="px-4 py-3 border-b border-border/50 font-semibold">Represents</th>
61
- </tr>
62
- </thead>
63
- <tbody className="divide-y divide-border/50">
64
- <tr className="bg-background hover:bg-muted/30 transition-colors">
65
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Req()</td>
66
- <td className="px-4 py-3 text-muted-foreground">The raw HTTP Request object</td>
67
- </tr>
68
- <tr className="bg-background hover:bg-muted/30 transition-colors">
69
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Res()</td>
70
- <td className="px-4 py-3 text-muted-foreground">The raw HTTP Response object</td>
71
- </tr>
72
- <tr className="bg-background hover:bg-muted/30 transition-colors">
73
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Param(key?)</td>
74
- <td className="px-4 py-3 text-muted-foreground">URL Path Parameters (e.g. req.params.id)</td>
75
- </tr>
76
- <tr className="bg-background hover:bg-muted/30 transition-colors">
77
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Body(key?)</td>
78
- <td className="px-4 py-3 text-muted-foreground">Parsed Request Body</td>
79
- </tr>
80
- <tr className="bg-background hover:bg-muted/30 transition-colors">
81
- <td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Query(key?)</td>
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 CreateUserSchema = tex.object({
97
+ export const createUserSchema = tex.object({
142
98
  name: tex.string({ min: 2 }),
143
99
  email: tex.email(),
144
- role: tex.enum(['admin', 'user'], { default: 'user' }),
100
+ role: tex.enum(['admin', 'member'], { default: 'member' }),
145
101
  })
146
102
 
147
- export type CreateUserDto = ResolveSchema<typeof CreateUserSchema>
103
+ export type CreateUserDto = ResolveSchema<typeof createUserSchema>
148
104
 
149
- @Controller('users')
105
+ @Controller()
150
106
  export default class UsersController {
151
- // 1. Parameter-level validation schema
152
- @Post('/')
107
+ @Post('/', { body: createUserSchema })
153
108
  @HttpCode(201)
154
- async create(@Body(CreateUserSchema) dto: CreateUserDto) {
155
- return { id: 3, ...dto }
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 can inject services using either the ergonomic `inject()` helper or standard constructor injection:
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 { UsersService } from './service'
124
+ import { UserService } from './service'
174
125
 
175
- @Controller('users')
126
+ @Controller()
176
127
  export default class UsersController {
177
- // Approach A: Property-level injection (Recommended)
178
- private usersService = inject(UsersService)
128
+ // 1. Property-level injection
129
+ private userService = inject(UserService)
179
130
 
180
- // Approach B: Constructor injection
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.usersService.findById(id)
139
+ return this.userService.getUserById(id)
189
140
  }
190
141
  }
191
142
  ```
192
143
 
193
- ## File Uploads
144
+ ---
194
145
 
195
- You can process file uploads in OOP controllers using `@UseUpload` or `@Upload` decorators alongside `@UploadedFile()` and `@UploadedFiles()`:
146
+ ## Parameter Transformation with Built-In Pipes
196
147
 
197
- ```typescript filename="src/http/users/route.ts"
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
- @Controller('users')
202
- export default class UsersController {
203
- @Post('/avatar')
204
- @UseUpload({
205
- field: 'avatar',
206
- maxBytes: 5 * 1024 * 1024,
207
- mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
208
- })
209
- async uploadAvatar(
210
- @UploadedFile() file: IUploadedFile,
211
- @Body() body: { caption?: string }
212
- ) {
213
- return {
214
- success: true,
215
- filename: file.filename,
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
- ```typescript filename="src/http/files/route.ts"
228
- import { Controller, Get, Res } from 'exisjs/decorators'
229
- import type { Response } from 'exisjs/router'
230
- import fs from 'node:fs'
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
- @Controller('files')
233
- export default class FilesController {
234
- // 1. Force file download
235
- @Get('/download')
236
- download(@Res() res: Response) {
237
- res.download('./reports/annual.pdf', 'Annual-Report.pdf')
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
- // 2. Stream Node.js Readable or auto-pipe return value
241
- @Get('/stream')
242
- streamCsv() {
243
- return fs.createReadStream('./data/export.csv')
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
- // 3. Server-Sent Events (SSE)
247
- @Get('/events')
248
- events(@Res() res: Response) {
249
- const sse = res.sse()
250
- sse.send({ event: 'ping', data: { time: Date.now() } })
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
- ## Status Codes & Headers
198
+ ---
256
199
 
257
- By default, responses have a `200 OK` status (except POST which uses `201`). You can change this declaratively using the `@HttpCode()` and `@Header()` decorators.
200
+ ## Status Codes, Headers & Redirects
258
201
 
259
- ```typescript filename="src/http/users/route.ts"
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
- @Controller('users')
263
- export default class UsersController {
264
- @Post()
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
- create() {
268
- return null // No Content
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
- ## Extended Features
222
+ ---
274
223
 
275
- ExisJS provides routing enhancements like Subdomain Routing, Redirects, and Passthrough Responses. These are supported across both paradigms.
224
+ ## Multipart File Uploads
276
225
 
277
- ### Functional Paradigm
226
+ Handle file uploads using the `@Upload` (or `@UseUpload`) decorator alongside `@UploadedFile()` and `@UploadedFiles()`:
278
227
 
279
- ```typescript filename="src/http/advanced/route.ts"
280
- import { controller, route } from 'exisjs/router'
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
- export default controller({
283
- // 1. Subdomain Routing
284
- ping: route.get('/ping', {
285
- host: ':sub.exis.local',
286
- handle({ req }) {
287
- return { subdomain: (req.params as any).sub }
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
- ### Class-Based (OOP) Paradigm
307
-
308
- ```typescript filename="src/http/advanced/route.ts"
309
- import { Controller, Get, HostParam, Redirect, Res } from 'exisjs/decorators'
310
- import type { Response } from 'exisjs/router'
311
-
312
- // 1. Subdomain Routing
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
- ## Registration & Library-Specific Response
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
- @Controller('users')
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
- ## Controller Inheritance (Base & Abstract Controllers)
257
+ ExisJS supports Object-Oriented class inheritance. Subclasses inherit all route handlers, parameter decorators, validation rules, and guards from base controllers:
357
258
 
358
- ExisJS supports full OOP inheritance for controllers. You can create generic base or abstract CRUD controllers and inherit all route handlers, parameter decorators, middlewares, guards, interceptors, and filters down to subclasses:
259
+ ### Base Generic Controller
359
260
 
360
261
  ```typescript filename="src/http/base-crud.controller.ts"
361
- import { Get, Post, Delete, Param, Body } from 'exisjs/decorators'
262
+ import { Get, Delete, Param } from 'exisjs/decorators'
362
263
 
363
264
  export abstract class BaseCrudController<T> {
364
- protected abstract resourceName: string
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 { id, resource: this.resourceName } as any
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
- return { deleted: true }
283
+ const deleted = await this.service.remove(id)
284
+ return { deleted }
379
285
  }
380
286
  }
381
287
  ```
382
288
 
383
- Subclasses inherit all parent endpoints and can add new routes or selectively override base methods:
289
+ ### Concrete Controller Subclass
384
290
 
385
291
  ```typescript filename="src/http/users/route.ts"
386
- import { Controller, Post, Body, Param } from 'exisjs/decorators'
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
- export interface User {
390
- id: string
391
- name: string
392
- }
393
-
394
- @Controller('/users')
296
+ @Controller()
395
297
  export default class UsersController extends BaseCrudController<User> {
396
- protected resourceName = 'users'
298
+ constructor(protected readonly service: UserService) {
299
+ super()
300
+ }
397
301
 
398
- // Subclass defines additional route
302
+ // Add specialized endpoint
399
303
  @Post('/')
400
304
  async create(@Body() user: User) {
401
- return { created: true, user }
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
- ### Functional Controller Inheritance (`controller.extend`)
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
- <Info title="Tip: Safely Returning with @Res">
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.