@moostjs/event-http 0.6.6 → 0.6.8
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/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +7 -13
- package/scripts/setup-skills.js +0 -76
- package/skills/moostjs-event-http/SKILL.md +0 -32
- package/skills/moostjs-event-http/auth.md +0 -275
- package/skills/moostjs-event-http/core.md +0 -193
- package/skills/moostjs-event-http/request.md +0 -230
- package/skills/moostjs-event-http/response.md +0 -287
- package/skills/moostjs-event-http/routing.md +0 -210
package/dist/index.cjs
CHANGED
|
@@ -695,7 +695,7 @@ const CONTEXT_TYPE = "HTTP";
|
|
|
695
695
|
image: "https://moost.org/moost-full-logo.svg",
|
|
696
696
|
link: "https://moost.org/",
|
|
697
697
|
poweredBy: "moostjs",
|
|
698
|
-
version: "0.6.
|
|
698
|
+
version: "0.6.7"
|
|
699
699
|
});
|
|
700
700
|
if (httpApp && httpApp instanceof __wooksjs_event_http.WooksHttp) this.httpApp = httpApp;
|
|
701
701
|
else if (httpApp) this.httpApp = (0, __wooksjs_event_http.createHttpApp)({
|
package/dist/index.mjs
CHANGED
|
@@ -672,7 +672,7 @@ const CONTEXT_TYPE = "HTTP";
|
|
|
672
672
|
image: "https://moost.org/moost-full-logo.svg",
|
|
673
673
|
link: "https://moost.org/",
|
|
674
674
|
poweredBy: "moostjs",
|
|
675
|
-
version: "0.6.
|
|
675
|
+
version: "0.6.7"
|
|
676
676
|
});
|
|
677
677
|
if (httpApp && httpApp instanceof WooksHttp) this.httpApp = httpApp;
|
|
678
678
|
else if (httpApp) this.httpApp = createHttpApp({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moostjs/event-http",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "@moostjs/event-http",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"composables",
|
|
@@ -21,13 +21,8 @@
|
|
|
21
21
|
"url": "git+https://github.com/moostjs/moostjs.git",
|
|
22
22
|
"directory": "packages/event-http"
|
|
23
23
|
},
|
|
24
|
-
"bin": {
|
|
25
|
-
"moostjs-event-http-skill": "./scripts/setup-skills.js"
|
|
26
|
-
},
|
|
27
24
|
"files": [
|
|
28
|
-
"dist"
|
|
29
|
-
"skills",
|
|
30
|
-
"scripts/setup-skills.js"
|
|
25
|
+
"dist"
|
|
31
26
|
],
|
|
32
27
|
"type": "module",
|
|
33
28
|
"sideEffects": false,
|
|
@@ -43,20 +38,19 @@
|
|
|
43
38
|
}
|
|
44
39
|
},
|
|
45
40
|
"dependencies": {
|
|
46
|
-
"@wooksjs/event-http": "^0.7.
|
|
47
|
-
"@wooksjs/http-body": "^0.7.
|
|
41
|
+
"@wooksjs/event-http": "^0.7.10",
|
|
42
|
+
"@wooksjs/http-body": "^0.7.10"
|
|
48
43
|
},
|
|
49
44
|
"devDependencies": {
|
|
50
45
|
"vitest": "3.2.4"
|
|
51
46
|
},
|
|
52
47
|
"peerDependencies": {
|
|
53
48
|
"@prostojs/infact": "^0.4.1",
|
|
54
|
-
"@wooksjs/event-core": "^0.7.
|
|
55
|
-
"moost": "^0.6.
|
|
49
|
+
"@wooksjs/event-core": "^0.7.10",
|
|
50
|
+
"moost": "^0.6.8"
|
|
56
51
|
},
|
|
57
52
|
"scripts": {
|
|
58
53
|
"pub": "pnpm publish --access public",
|
|
59
|
-
"test": "vitest"
|
|
60
|
-
"setup-skills": "node ./scripts/setup-skills.js"
|
|
54
|
+
"test": "vitest"
|
|
61
55
|
}
|
|
62
56
|
}
|
package/scripts/setup-skills.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* prettier-ignore */
|
|
3
|
-
import fs from 'fs'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import os from 'os'
|
|
6
|
-
import { fileURLToPath } from 'url'
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
|
|
10
|
-
const SKILL_NAME = 'moostjs-event-http'
|
|
11
|
-
const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
|
|
12
|
-
|
|
13
|
-
if (!fs.existsSync(SKILL_SRC)) {
|
|
14
|
-
console.error(`No skills found at ${SKILL_SRC}`)
|
|
15
|
-
console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
|
|
16
|
-
process.exit(1)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const AGENTS = {
|
|
20
|
-
'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
|
|
21
|
-
'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
|
|
22
|
-
'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
|
|
23
|
-
'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
|
|
24
|
-
'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const args = process.argv.slice(2)
|
|
28
|
-
const isGlobal = args.includes('--global') || args.includes('-g')
|
|
29
|
-
const isPostinstall = args.includes('--postinstall')
|
|
30
|
-
let installed = 0, skipped = 0
|
|
31
|
-
const installedDirs = []
|
|
32
|
-
|
|
33
|
-
for (const [agentName, cfg] of Object.entries(AGENTS)) {
|
|
34
|
-
const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
|
|
35
|
-
const agentRootDir = path.dirname(cfg.global)
|
|
36
|
-
|
|
37
|
-
if (isPostinstall || isGlobal) {
|
|
38
|
-
if (!fs.existsSync(agentRootDir)) { skipped++; continue }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const dest = path.join(targetBase, SKILL_NAME)
|
|
42
|
-
try {
|
|
43
|
-
fs.mkdirSync(dest, { recursive: true })
|
|
44
|
-
fs.cpSync(SKILL_SRC, dest, { recursive: true })
|
|
45
|
-
console.log(`[${SKILL_NAME}] installed to ${dest}`)
|
|
46
|
-
installed++
|
|
47
|
-
if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.warn(`[${SKILL_NAME}] failed — ${err.message}`)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!isGlobal && installedDirs.length > 0) {
|
|
54
|
-
const gitignorePath = path.join(process.cwd(), '.gitignore')
|
|
55
|
-
let gitignoreContent = ''
|
|
56
|
-
try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
|
|
57
|
-
const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
|
|
58
|
-
if (linesToAdd.length > 0) {
|
|
59
|
-
const hasHeader = gitignoreContent.includes('# AI agent skills')
|
|
60
|
-
const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
|
|
61
|
-
+ (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
|
|
62
|
-
+ linesToAdd.join('\n') + '\n'
|
|
63
|
-
fs.appendFileSync(gitignorePath, block)
|
|
64
|
-
console.log(`[${SKILL_NAME}] added entries to .gitignore`)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (installed === 0 && isPostinstall) {
|
|
69
|
-
// Silence is fine — no agents present, nothing to do
|
|
70
|
-
} else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
|
|
71
|
-
console.log('No agent directories detected. Try --global or run without it for project-local install.')
|
|
72
|
-
} else if (installed === 0) {
|
|
73
|
-
console.log('Nothing installed. Run without --global to install project-locally.')
|
|
74
|
-
} else {
|
|
75
|
-
console.log(`Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
|
|
76
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: moostjs-event-http
|
|
3
|
-
description: Use this skill when working with @moostjs/event-http — to create an HTTP server with MoostHttp adapter, register route handlers with @Get()/@Post()/@Put()/@Delete()/@Patch()/@All(), extract request data with @Query(), @Header(), @Cookie(), @Body(), @RawBody(), @Authorization(), @Url(), @Method(), @Req(), @Res(), @ReqId(), @Ip(), @IpList(), control responses with @SetHeader(), @SetCookie(), @SetStatus(), @StatusRef(), @HeaderRef(), @CookieRef(), @CookieAttrsRef(), throw HTTP errors with HttpError, enforce body limits with @BodySizeLimit()/@CompressedBodySizeLimit()/@BodyReadTimeoutMs(), define auth guards with defineAuthGuard()/AuthGuard/Authenticate, or handle WebSocket upgrades with @Upgrade().
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# @moostjs/event-http
|
|
7
|
-
|
|
8
|
-
Moost HTTP adapter — decorator-driven HTTP server built on `@wooksjs/event-http`. Provides route decorators, request data extractors, response control, auth guards, and body limits for Moost applications.
|
|
9
|
-
|
|
10
|
-
## How to use this skill
|
|
11
|
-
|
|
12
|
-
Read the domain file that matches the task. Do not load all files — only what you need.
|
|
13
|
-
|
|
14
|
-
| Domain | File | Load when... |
|
|
15
|
-
|--------|------|------------|
|
|
16
|
-
| Core concepts & setup | [core.md](core.md) | Starting a new project, understanding the mental model, configuring MoostHttp adapter |
|
|
17
|
-
| Routing & handlers | [routing.md](routing.md) | Defining routes with @Get/@Post/etc, route parameters, wildcards, path patterns |
|
|
18
|
-
| Request data | [request.md](request.md) | Extracting query params, headers, cookies, body, auth, IP, URL from requests |
|
|
19
|
-
| Response control | [response.md](response.md) | Setting status codes, headers, cookies, error handling, raw response access |
|
|
20
|
-
| Authentication | [auth.md](auth.md) | Auth guards, credential extraction, bearer/basic/apiKey/cookie transports, @Authenticate |
|
|
21
|
-
|
|
22
|
-
## Quick reference
|
|
23
|
-
|
|
24
|
-
```ts
|
|
25
|
-
// Imports
|
|
26
|
-
import { MoostHttp, Get, Post, Put, Delete, Patch, All, HttpMethod, Upgrade } from '@moostjs/event-http'
|
|
27
|
-
import { Query, Header, Cookie, Body, RawBody, Authorization, Url, Method, Req, Res, ReqId, Ip, IpList } from '@moostjs/event-http'
|
|
28
|
-
import { SetHeader, SetCookie, SetStatus, StatusRef, HeaderRef, CookieRef, CookieAttrsRef } from '@moostjs/event-http'
|
|
29
|
-
import { BodySizeLimit, CompressedBodySizeLimit, BodyReadTimeoutMs } from '@moostjs/event-http'
|
|
30
|
-
import { Authenticate, AuthGuard, defineAuthGuard, HttpError } from '@moostjs/event-http'
|
|
31
|
-
import { Controller, Param, Params } from 'moost'
|
|
32
|
-
```
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
# Authentication — @moostjs/event-http
|
|
2
|
-
|
|
3
|
-
> Declarative auth guards with automatic credential extraction and Swagger integration.
|
|
4
|
-
|
|
5
|
-
## Concepts
|
|
6
|
-
|
|
7
|
-
The auth guard system has three components:
|
|
8
|
-
|
|
9
|
-
1. **Transport declaration** — describes *where* credentials come from (bearer token, basic auth, API key, cookie)
|
|
10
|
-
2. **Guard handler** — your verification logic, receives the extracted credentials
|
|
11
|
-
3. **`@Authenticate` decorator** — applies the guard to a controller or handler
|
|
12
|
-
|
|
13
|
-
Two APIs are provided:
|
|
14
|
-
- **Functional** (`defineAuthGuard`) — stateless, simple guards
|
|
15
|
-
- **Class-based** (`AuthGuard`) — when you need dependency injection
|
|
16
|
-
|
|
17
|
-
Auth guard transport declarations are stored in metadata, enabling automatic Swagger/OpenAPI security scheme discovery.
|
|
18
|
-
|
|
19
|
-
## API Reference
|
|
20
|
-
|
|
21
|
-
### `defineAuthGuard(transports, handler)`
|
|
22
|
-
|
|
23
|
-
Create a functional auth guard.
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
import { defineAuthGuard, HttpError } from '@moostjs/event-http'
|
|
27
|
-
|
|
28
|
-
const jwtGuard = defineAuthGuard(
|
|
29
|
-
{ bearer: { format: 'JWT' } },
|
|
30
|
-
(transports) => {
|
|
31
|
-
const user = verifyJwt(transports.bearer)
|
|
32
|
-
if (!user) throw new HttpError(401, 'Invalid token')
|
|
33
|
-
// return value is optional — if returned, it becomes the handler's response (short-circuit)
|
|
34
|
-
},
|
|
35
|
-
)
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
**Parameters:**
|
|
39
|
-
- `transports: TAuthTransportDeclaration` — which credentials to extract
|
|
40
|
-
- `handler: (transports: TAuthTransportValues<T>) => unknown | Promise<unknown>` — verification logic
|
|
41
|
-
|
|
42
|
-
**Returns:** `TAuthGuardDef` — an interceptor def with transport metadata attached.
|
|
43
|
-
|
|
44
|
-
### `AuthGuard<T>`
|
|
45
|
-
|
|
46
|
-
Abstract base class for class-based auth guards. Use when you need DI.
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
import { AuthGuard, HttpError } from '@moostjs/event-http'
|
|
50
|
-
import { Injectable } from 'moost'
|
|
51
|
-
|
|
52
|
-
@Injectable()
|
|
53
|
-
class JwtGuard extends AuthGuard<{ bearer: { format: 'JWT' } }> {
|
|
54
|
-
static transports = { bearer: { format: 'JWT' } } as const
|
|
55
|
-
|
|
56
|
-
constructor(private userService: UserService) {}
|
|
57
|
-
|
|
58
|
-
handle(transports: { bearer: string }) {
|
|
59
|
-
const user = this.userService.verifyToken(transports.bearer)
|
|
60
|
-
if (!user) throw new HttpError(401, 'Invalid token')
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Requirements:
|
|
66
|
-
- Extend `AuthGuard<T>` with transport declaration as generic parameter
|
|
67
|
-
- Set `static transports` matching the generic (read at runtime)
|
|
68
|
-
- Implement `handle(transports)` with verification logic
|
|
69
|
-
- Use `@Injectable()` for constructor injection
|
|
70
|
-
|
|
71
|
-
### `@Authenticate(handler)`
|
|
72
|
-
|
|
73
|
-
Apply an auth guard to a controller or handler method.
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
import { Authenticate, Get } from '@moostjs/event-http'
|
|
77
|
-
import { Controller } from 'moost'
|
|
78
|
-
|
|
79
|
-
// Controller-level — all handlers require auth
|
|
80
|
-
@Authenticate(jwtGuard)
|
|
81
|
-
@Controller('users')
|
|
82
|
-
class UsersController {
|
|
83
|
-
@Get('')
|
|
84
|
-
list() {}
|
|
85
|
-
|
|
86
|
-
@Get(':id')
|
|
87
|
-
find() {}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Handler-level — specific endpoints only
|
|
91
|
-
@Controller('products')
|
|
92
|
-
class ProductsController {
|
|
93
|
-
@Get('')
|
|
94
|
-
list() { /* public */ }
|
|
95
|
-
|
|
96
|
-
@Authenticate(jwtGuard)
|
|
97
|
-
@Post('')
|
|
98
|
-
create() { /* requires auth */ }
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Accepts both functional (`TAuthGuardDef`) and class-based (`TAuthGuardClass`) guards.
|
|
103
|
-
|
|
104
|
-
### `extractTransports(declaration)`
|
|
105
|
-
|
|
106
|
-
Low-level function that extracts credentials from the current request context. Called internally by auth guards — rarely needed directly.
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
import { extractTransports } from '@moostjs/event-http'
|
|
110
|
-
|
|
111
|
-
const values = extractTransports({ bearer: { format: 'JWT' } })
|
|
112
|
-
// values.bearer = 'eyJ...' (the raw token)
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Throws `HttpError(401, 'No authentication credentials provided')` if none of the declared transports are present.
|
|
116
|
-
|
|
117
|
-
## Transport types
|
|
118
|
-
|
|
119
|
-
### Bearer token
|
|
120
|
-
|
|
121
|
-
Extracts from `Authorization: Bearer <token>`:
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
{ bearer: { format?: string, description?: string } }
|
|
125
|
-
// Extracted value: string (raw token without "Bearer " prefix)
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Basic authentication
|
|
129
|
-
|
|
130
|
-
Extracts from `Authorization: Basic <base64>`:
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
{ basic: { description?: string } }
|
|
134
|
-
// Extracted value: { username: string, password: string }
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### API key
|
|
138
|
-
|
|
139
|
-
Extracts from a header, query parameter, or cookie:
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
{ apiKey: { name: string, in: 'header' | 'query' | 'cookie', description?: string } }
|
|
143
|
-
// Extracted value: string
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
// From header
|
|
148
|
-
defineAuthGuard({ apiKey: { name: 'X-API-Key', in: 'header' } }, (t) => { t.apiKey /* string */ })
|
|
149
|
-
|
|
150
|
-
// From query param
|
|
151
|
-
defineAuthGuard({ apiKey: { name: 'api_key', in: 'query' } }, (t) => { t.apiKey /* string */ })
|
|
152
|
-
|
|
153
|
-
// From cookie
|
|
154
|
-
defineAuthGuard({ apiKey: { name: 'api_key', in: 'cookie' } }, (t) => { t.apiKey /* string */ })
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Cookie
|
|
158
|
-
|
|
159
|
-
Extracts a value from a named cookie:
|
|
160
|
-
|
|
161
|
-
```ts
|
|
162
|
-
{ cookie: { name: string, description?: string } }
|
|
163
|
-
// Extracted value: string
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Common Patterns
|
|
167
|
-
|
|
168
|
-
### Pattern: JWT bearer guard
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
const jwtGuard = defineAuthGuard(
|
|
172
|
-
{ bearer: { format: 'JWT', description: 'JWT access token' } },
|
|
173
|
-
(transports) => {
|
|
174
|
-
const payload = verifyJwt(transports.bearer)
|
|
175
|
-
if (!payload) throw new HttpError(401, 'Invalid or expired token')
|
|
176
|
-
},
|
|
177
|
-
)
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Pattern: API key guard
|
|
181
|
-
|
|
182
|
-
```ts
|
|
183
|
-
const apiKeyGuard = defineAuthGuard(
|
|
184
|
-
{ apiKey: { name: 'X-API-Key', in: 'header' } },
|
|
185
|
-
(transports) => {
|
|
186
|
-
if (!isValidApiKey(transports.apiKey)) {
|
|
187
|
-
throw new HttpError(401, 'Invalid API key')
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
)
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Pattern: Auth + authorization stacking
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
@Authenticate(jwtGuard) // step 1: verify credentials
|
|
197
|
-
@RequireRole('admin') // step 2: check authorization
|
|
198
|
-
@Controller('admin')
|
|
199
|
-
class AdminController {
|
|
200
|
-
@Get('dashboard')
|
|
201
|
-
dashboard() { /* authenticated + admin */ }
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### Pattern: Class-based guard with DI
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
@Injectable()
|
|
209
|
-
class SessionGuard extends AuthGuard<{ cookie: { name: 'session' } }> {
|
|
210
|
-
static transports = { cookie: { name: 'session' } } as const
|
|
211
|
-
|
|
212
|
-
constructor(private sessionService: SessionService) {}
|
|
213
|
-
|
|
214
|
-
handle(transports: { cookie: string }) {
|
|
215
|
-
const session = this.sessionService.validate(transports.cookie)
|
|
216
|
-
if (!session) throw new HttpError(401, 'Invalid session')
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
@Authenticate(SessionGuard)
|
|
221
|
-
@Controller('dashboard')
|
|
222
|
-
class DashboardController {}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Pattern: Handler-level override
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
@Authenticate(apiKeyGuard)
|
|
229
|
-
@Controller('products')
|
|
230
|
-
class ProductsController {
|
|
231
|
-
@Get('')
|
|
232
|
-
list() { /* uses apiKeyGuard */ }
|
|
233
|
-
|
|
234
|
-
@Authenticate(basicGuard)
|
|
235
|
-
@Post('')
|
|
236
|
-
create() { /* uses basicGuard instead */ }
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## Integration
|
|
241
|
-
|
|
242
|
-
- **Swagger**: Transport declarations map directly to OpenAPI security schemes. The `@moostjs/swagger` package auto-discovers `@Authenticate` metadata.
|
|
243
|
-
- **Guards**: Auth guards run at `GUARD` priority. Combine with custom authorization guards (also at `GUARD` priority) — they execute in decorator declaration order.
|
|
244
|
-
|
|
245
|
-
## Types
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
interface TAuthTransportDeclaration {
|
|
249
|
-
bearer?: { format?: string; description?: string }
|
|
250
|
-
basic?: { description?: string }
|
|
251
|
-
apiKey?: { name: string; in: 'header' | 'query' | 'cookie'; description?: string }
|
|
252
|
-
cookie?: { name: string; description?: string }
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
type TAuthTransportValues<T> = {
|
|
256
|
-
[K in keyof T]: K extends 'basic' ? { username: string; password: string } : string
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
type TAuthGuardHandler = TAuthGuardDef | TAuthGuardClass
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
## Best Practices
|
|
263
|
-
|
|
264
|
-
- Use functional guards (`defineAuthGuard`) for simple, stateless checks
|
|
265
|
-
- Use class-based guards (`AuthGuard`) when you need DI services (e.g., database, token service)
|
|
266
|
-
- Apply `@Authenticate` at the controller level for protected resources, handler level for mixed access
|
|
267
|
-
- Always throw `HttpError` with meaningful messages for auth failures
|
|
268
|
-
- Combine with `@moostjs/swagger` for automatic OpenAPI security documentation
|
|
269
|
-
|
|
270
|
-
## Gotchas
|
|
271
|
-
|
|
272
|
-
- If none of the declared transports are present in the request, `extractTransports` throws `HttpError(401)` automatically — your handler won't be called
|
|
273
|
-
- Class-based guards must set `static transports` — it's read at runtime, not from the generic parameter
|
|
274
|
-
- Auth guards run at `GUARD` priority — they execute before `INTERCEPTOR`-priority interceptors
|
|
275
|
-
- The handler's return value (if any) short-circuits the response — use this for redirects or custom auth responses
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
# Core concepts & setup — @moostjs/event-http
|
|
2
|
-
|
|
3
|
-
> How to create and configure an HTTP server with the Moost HTTP adapter.
|
|
4
|
-
|
|
5
|
-
## Concepts
|
|
6
|
-
|
|
7
|
-
`@moostjs/event-http` is the HTTP adapter for the Moost framework. It bridges Moost's decorator-driven controller system with `@wooksjs/event-http` (the underlying HTTP engine). The adapter handles:
|
|
8
|
-
|
|
9
|
-
- Binding decorated handler methods to HTTP routes
|
|
10
|
-
- Managing request scoping and cleanup
|
|
11
|
-
- Providing dependency injection for the underlying WooksHttp instance
|
|
12
|
-
|
|
13
|
-
**Key classes:**
|
|
14
|
-
- `MoostHttp` — the adapter class you instantiate and attach to your Moost app
|
|
15
|
-
- `WooksHttp` — the underlying Wooks HTTP engine (available via DI if needed)
|
|
16
|
-
|
|
17
|
-
## Installation
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npm install @moostjs/event-http moost
|
|
21
|
-
# or
|
|
22
|
-
pnpm add @moostjs/event-http moost
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Peer dependencies (installed automatically with moost):
|
|
26
|
-
- `@wooksjs/event-core` — async event context
|
|
27
|
-
- `@prostojs/infact` — dependency injection
|
|
28
|
-
- `@prostojs/router` — route matching
|
|
29
|
-
|
|
30
|
-
## Setup
|
|
31
|
-
|
|
32
|
-
### Minimal HTTP server
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
import { MoostHttp, Get } from '@moostjs/event-http'
|
|
36
|
-
import { Moost, Controller, Param } from 'moost'
|
|
37
|
-
|
|
38
|
-
@Controller()
|
|
39
|
-
class AppController {
|
|
40
|
-
@Get('hello/:name')
|
|
41
|
-
greet(@Param('name') name: string) {
|
|
42
|
-
return `Hello, ${name}!`
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const app = new Moost()
|
|
47
|
-
void app.adapter(new MoostHttp()).listen(3000, () => {
|
|
48
|
-
app.getLogger('app').info('Up on port 3000')
|
|
49
|
-
})
|
|
50
|
-
void app.registerControllers(AppController).init()
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Scaffold a project
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
npm create moost -- --http
|
|
57
|
-
# or with a name:
|
|
58
|
-
npm create moost my-web-app -- --http
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## API Reference
|
|
62
|
-
|
|
63
|
-
### `MoostHttp`
|
|
64
|
-
|
|
65
|
-
The HTTP adapter class implementing `TMoostAdapter`.
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
import { MoostHttp } from '@moostjs/event-http'
|
|
69
|
-
|
|
70
|
-
// Default — creates a new WooksHttp internally
|
|
71
|
-
const http = new MoostHttp()
|
|
72
|
-
|
|
73
|
-
// With options passed to WooksHttp
|
|
74
|
-
const http = new MoostHttp({ onNotFound: customHandler })
|
|
75
|
-
|
|
76
|
-
// With an existing WooksHttp instance
|
|
77
|
-
const http = new MoostHttp(existingWooksHttp)
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**Methods:**
|
|
81
|
-
|
|
82
|
-
| Method | Returns | Description |
|
|
83
|
-
|--------|---------|-------------|
|
|
84
|
-
| `listen(port?, ...)` | `Promise<void>` | Start the HTTP server (same overloads as `net.Server.listen`) |
|
|
85
|
-
| `getHttpApp()` | `WooksHttp` | Access the underlying Wooks HTTP engine |
|
|
86
|
-
| `getServerCb()` | `RequestListener` | Get the request handler callback for use with existing Node.js servers |
|
|
87
|
-
|
|
88
|
-
### `getServerCb()` — Custom server integration
|
|
89
|
-
|
|
90
|
-
Use `getServerCb()` to integrate with an existing Node.js HTTP/HTTPS server:
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
import { createServer } from 'https'
|
|
94
|
-
import { MoostHttp } from '@moostjs/event-http'
|
|
95
|
-
import { Moost } from 'moost'
|
|
96
|
-
|
|
97
|
-
const http = new MoostHttp()
|
|
98
|
-
const app = new Moost()
|
|
99
|
-
app.adapter(http)
|
|
100
|
-
|
|
101
|
-
const server = createServer(tlsOptions, http.getServerCb())
|
|
102
|
-
server.listen(443)
|
|
103
|
-
await app.init()
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### `HttpError`
|
|
107
|
-
|
|
108
|
-
Re-exported from `@wooksjs/event-http`. Throw to produce an HTTP error response with a specific status code.
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
import { HttpError } from '@moostjs/event-http'
|
|
112
|
-
|
|
113
|
-
throw new HttpError(404, 'Not Found')
|
|
114
|
-
throw new HttpError(422, { message: 'Validation failed', errors: [...] })
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### `httpKind`
|
|
118
|
-
|
|
119
|
-
Re-exported from `@wooksjs/event-http`. The event kind identifier for HTTP events.
|
|
120
|
-
|
|
121
|
-
### `useHttpContext()`
|
|
122
|
-
|
|
123
|
-
Re-exported from `@wooksjs/event-http`. Access the raw Wooks HTTP context from within a handler (advanced use only — prefer decorators).
|
|
124
|
-
|
|
125
|
-
## Common Patterns
|
|
126
|
-
|
|
127
|
-
### Pattern: Class extending Moost
|
|
128
|
-
|
|
129
|
-
Instead of creating a separate `Moost` instance and registering controllers, extend `Moost` directly:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
import { MoostHttp, Get } from '@moostjs/event-http'
|
|
133
|
-
import { Moost, Param } from 'moost'
|
|
134
|
-
|
|
135
|
-
class MyServer extends Moost {
|
|
136
|
-
@Get('test/:name')
|
|
137
|
-
test(@Param('name') name: string) {
|
|
138
|
-
return { message: `Hello ${name}!` }
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const app = new MyServer()
|
|
143
|
-
app.adapter(new MoostHttp()).listen(3000)
|
|
144
|
-
void app.init()
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Pattern: Multiple controllers
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
import { Moost } from 'moost'
|
|
151
|
-
import { MoostHttp } from '@moostjs/event-http'
|
|
152
|
-
|
|
153
|
-
const app = new Moost()
|
|
154
|
-
app.adapter(new MoostHttp()).listen(3000)
|
|
155
|
-
void app
|
|
156
|
-
.registerControllers(UserController, ProductController, OrderController)
|
|
157
|
-
.init()
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## DI-available services
|
|
161
|
-
|
|
162
|
-
When MoostHttp is attached, these are available via constructor injection:
|
|
163
|
-
|
|
164
|
-
| Token | Type | Description |
|
|
165
|
-
|-------|------|-------------|
|
|
166
|
-
| `WooksHttp` | class | The underlying Wooks HTTP app |
|
|
167
|
-
| `'WooksHttp'` | string | Same, by string token |
|
|
168
|
-
| `HttpServer` | `http.Server` | The Node.js HTTP server instance |
|
|
169
|
-
| `HttpsServer` | `https.Server` | The Node.js HTTPS server instance |
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
import { Injectable } from 'moost'
|
|
173
|
-
import { WooksHttp } from '@wooksjs/event-http'
|
|
174
|
-
|
|
175
|
-
@Injectable()
|
|
176
|
-
class MyService {
|
|
177
|
-
constructor(private wooks: WooksHttp) {}
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
## Best Practices
|
|
182
|
-
|
|
183
|
-
- Call `app.adapter(http).listen(port)` before `app.init()` — the adapter must be attached before initialization
|
|
184
|
-
- Use `registerControllers()` to add controller classes, not instances
|
|
185
|
-
- Prefer the decorator-based API (`@Get`, `@Body`, etc.) over accessing wooks composables directly
|
|
186
|
-
- Use `getServerCb()` when integrating with existing HTTP/HTTPS servers instead of calling `listen()`
|
|
187
|
-
|
|
188
|
-
## Gotchas
|
|
189
|
-
|
|
190
|
-
- The `path` argument in route decorators is optional — when omitted, the method name is used as the path segment
|
|
191
|
-
- `@Get('')` (empty string) maps to the controller root, while `@Get()` (no argument) uses the method name
|
|
192
|
-
- `app.init()` must be called after `registerControllers()` — routes are bound during initialization
|
|
193
|
-
- MoostHttp registers a default 404 handler that runs through the global interceptor chain
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
# Request data — @moostjs/event-http
|
|
2
|
-
|
|
3
|
-
> Extracting data from incoming HTTP requests using resolver decorators.
|
|
4
|
-
|
|
5
|
-
## Concepts
|
|
6
|
-
|
|
7
|
-
Moost provides **resolver decorators** that extract values from the incoming request and inject them as handler method parameters. Each decorator wraps a `@Resolve()` call that invokes the appropriate `@wooksjs/event-http` composable. All resolver decorators can also be used as **property decorators** on `FOR_EVENT`-scoped controllers.
|
|
8
|
-
|
|
9
|
-
## API Reference
|
|
10
|
-
|
|
11
|
-
### `@Query(name?)`
|
|
12
|
-
|
|
13
|
-
Extract query parameters. With a name, returns a single value; without, returns all as an object.
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
import { Get, Query } from '@moostjs/event-http'
|
|
17
|
-
|
|
18
|
-
@Get('search')
|
|
19
|
-
search(
|
|
20
|
-
@Query('q') query: string, // single param
|
|
21
|
-
@Query() params: Record<string, string>, // all params
|
|
22
|
-
) {}
|
|
23
|
-
// GET /search?q=moost&limit=10
|
|
24
|
-
// query = 'moost', params = { q: 'moost', limit: '10' }
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
- Returns `undefined` if the parameter is missing
|
|
28
|
-
- `@Query()` returns `undefined` (not `{}`) when there are no query params at all
|
|
29
|
-
- Sets metadata: `paramSource: 'QUERY_ITEM'` (named) or `'QUERY'` (all)
|
|
30
|
-
|
|
31
|
-
### `@Header(name)`
|
|
32
|
-
|
|
33
|
-
Extract a request header value.
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
import { Get, Header } from '@moostjs/event-http'
|
|
37
|
-
|
|
38
|
-
@Get('test')
|
|
39
|
-
test(@Header('content-type') contentType: string) {}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Header names are case-insensitive.
|
|
43
|
-
|
|
44
|
-
### `@Cookie(name)`
|
|
45
|
-
|
|
46
|
-
Extract a request cookie value.
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
import { Get, Cookie } from '@moostjs/event-http'
|
|
50
|
-
|
|
51
|
-
@Get('profile')
|
|
52
|
-
profile(@Cookie('session') session: string) {}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### `@Body()`
|
|
56
|
-
|
|
57
|
-
Parse and return the request body. Automatically detects JSON, form-encoded, and text content types.
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
import { Post, Body } from '@moostjs/event-http'
|
|
61
|
-
|
|
62
|
-
@Post('users')
|
|
63
|
-
create(@Body() data: { name: string, email: string }) {}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
- Sets metadata: `paramSource: 'BODY'`
|
|
67
|
-
- Uses `@wooksjs/http-body` for parsing
|
|
68
|
-
|
|
69
|
-
### `@RawBody()`
|
|
70
|
-
|
|
71
|
-
Get the raw request body as a `Buffer`.
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
import { Post, RawBody } from '@moostjs/event-http'
|
|
75
|
-
|
|
76
|
-
@Post('upload')
|
|
77
|
-
upload(@RawBody() raw: Buffer) {}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### `@Authorization(field)`
|
|
81
|
-
|
|
82
|
-
Extract parts of the `Authorization` header.
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
import { Get, Authorization } from '@moostjs/event-http'
|
|
86
|
-
|
|
87
|
-
@Get('profile')
|
|
88
|
-
profile(
|
|
89
|
-
@Authorization('bearer') token: string, // Bearer token (no prefix)
|
|
90
|
-
@Authorization('type') authType: string, // "Bearer", "Basic", etc.
|
|
91
|
-
@Authorization('username') user: string, // from Basic auth
|
|
92
|
-
@Authorization('password') pass: string, // from Basic auth
|
|
93
|
-
@Authorization('raw') credentials: string, // raw credentials string
|
|
94
|
-
) {}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Valid fields: `'username'`, `'password'`, `'bearer'`, `'raw'`, `'type'`
|
|
98
|
-
|
|
99
|
-
### `@Url()`
|
|
100
|
-
|
|
101
|
-
Get the requested URL string.
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
import { Get, Url } from '@moostjs/event-http'
|
|
105
|
-
|
|
106
|
-
@Get('info')
|
|
107
|
-
info(@Url() url: string) {}
|
|
108
|
-
// url = '/info?page=1'
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### `@Method()`
|
|
112
|
-
|
|
113
|
-
Get the HTTP method string.
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
import { All, Method } from '@moostjs/event-http'
|
|
117
|
-
|
|
118
|
-
@All('proxy')
|
|
119
|
-
proxy(@Method() method: string) {}
|
|
120
|
-
// method = 'GET', 'POST', etc.
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### `@Req()`
|
|
124
|
-
|
|
125
|
-
Get the raw Node.js `IncomingMessage`.
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
import { Get, Req } from '@moostjs/event-http'
|
|
129
|
-
import type { IncomingMessage } from 'http'
|
|
130
|
-
|
|
131
|
-
@Get('raw')
|
|
132
|
-
raw(@Req() request: IncomingMessage) {
|
|
133
|
-
return { httpVersion: request.httpVersion }
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### `@Res(opts?)`
|
|
138
|
-
|
|
139
|
-
Get the raw Node.js `ServerResponse`. When used, the framework does **not** process the return value.
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
import { Get, Res } from '@moostjs/event-http'
|
|
143
|
-
import type { ServerResponse } from 'http'
|
|
144
|
-
|
|
145
|
-
@Get('raw')
|
|
146
|
-
raw(@Res() res: ServerResponse) {
|
|
147
|
-
res.writeHead(200, { 'content-type': 'text/plain' })
|
|
148
|
-
res.end('Manual response')
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Pass `{ passthrough: true }` to get the raw response but still let the framework handle the return value:
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
@Get('hybrid')
|
|
156
|
-
hybrid(@Res({ passthrough: true }) res: ServerResponse) {
|
|
157
|
-
res.setHeader('x-custom', 'value')
|
|
158
|
-
return { data: 'processed by framework' }
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### `@ReqId()`
|
|
163
|
-
|
|
164
|
-
Get the unique request UUID.
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
import { Get, ReqId } from '@moostjs/event-http'
|
|
168
|
-
|
|
169
|
-
@Get('test')
|
|
170
|
-
test(@ReqId() requestId: string) {}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### `@Ip(opts?)`
|
|
174
|
-
|
|
175
|
-
Get the client IP address.
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
import { Get, Ip } from '@moostjs/event-http'
|
|
179
|
-
|
|
180
|
-
@Get('client')
|
|
181
|
-
client(
|
|
182
|
-
@Ip() ip: string, // direct client IP
|
|
183
|
-
@Ip({ trustProxy: true }) realIp: string, // considers x-forwarded-for
|
|
184
|
-
) {}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### `@IpList()`
|
|
188
|
-
|
|
189
|
-
Get the full IP address chain.
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
import { Get, IpList } from '@moostjs/event-http'
|
|
193
|
-
|
|
194
|
-
@Get('client')
|
|
195
|
-
client(@IpList() allIps: string[]) {}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
## Decorator summary table
|
|
199
|
-
|
|
200
|
-
| Decorator | Returns | Import |
|
|
201
|
-
|-----------|---------|--------|
|
|
202
|
-
| `@Param(name)` | Route parameter | `moost` |
|
|
203
|
-
| `@Params()` | All route params | `moost` |
|
|
204
|
-
| `@Query(name?)` | Query param(s) | `@moostjs/event-http` |
|
|
205
|
-
| `@Header(name)` | Header value | `@moostjs/event-http` |
|
|
206
|
-
| `@Cookie(name)` | Cookie value | `@moostjs/event-http` |
|
|
207
|
-
| `@Body()` | Parsed body | `@moostjs/event-http` |
|
|
208
|
-
| `@RawBody()` | Raw Buffer | `@moostjs/event-http` |
|
|
209
|
-
| `@Authorization(field)` | Auth field | `@moostjs/event-http` |
|
|
210
|
-
| `@Url()` | URL string | `@moostjs/event-http` |
|
|
211
|
-
| `@Method()` | HTTP method | `@moostjs/event-http` |
|
|
212
|
-
| `@ReqId()` | Request UUID | `@moostjs/event-http` |
|
|
213
|
-
| `@Ip(opts?)` | Client IP | `@moostjs/event-http` |
|
|
214
|
-
| `@IpList()` | IP chain | `@moostjs/event-http` |
|
|
215
|
-
| `@Req()` | IncomingMessage | `@moostjs/event-http` |
|
|
216
|
-
| `@Res(opts?)` | ServerResponse | `@moostjs/event-http` |
|
|
217
|
-
|
|
218
|
-
## Best Practices
|
|
219
|
-
|
|
220
|
-
- Use `@Authorization()` for quick header parsing; use `@Authenticate()` guards for production auth
|
|
221
|
-
- Prefer `@Body()` over `@RawBody()` unless you need the raw bytes
|
|
222
|
-
- Query values are always strings — use pipes to transform to numbers or other types
|
|
223
|
-
- Use `@Ip({ trustProxy: true })` behind reverse proxies to get the real client IP
|
|
224
|
-
|
|
225
|
-
## Gotchas
|
|
226
|
-
|
|
227
|
-
- `@Query('name')` returns `undefined` when the parameter is missing (not `null` or empty string)
|
|
228
|
-
- `@Query()` (all params) returns `undefined` when there are no query params, not an empty object
|
|
229
|
-
- `@Res()` without `passthrough` takes over the response — the handler's return value is ignored
|
|
230
|
-
- `@Param` and `@Params` are imported from `moost`, not from `@moostjs/event-http`
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
# Response control — @moostjs/event-http
|
|
2
|
-
|
|
3
|
-
> Setting status codes, headers, cookies, handling errors, and controlling HTTP responses.
|
|
4
|
-
|
|
5
|
-
## Concepts
|
|
6
|
-
|
|
7
|
-
Moost provides two styles for response control:
|
|
8
|
-
|
|
9
|
-
1. **Static decorators** (`@SetStatus`, `@SetHeader`, `@SetCookie`) — applied via interceptors at `AFTER_ALL` priority, declarative and fixed
|
|
10
|
-
2. **Dynamic refs** (`@StatusRef`, `@HeaderRef`, `@CookieRef`, `@CookieAttrsRef`) — Proxy-based reactive bindings, set values programmatically at runtime
|
|
11
|
-
|
|
12
|
-
Both work as method decorators. Ref decorators also work as parameter decorators and property decorators (on `FOR_EVENT`-scoped controllers).
|
|
13
|
-
|
|
14
|
-
## API Reference
|
|
15
|
-
|
|
16
|
-
### `@SetStatus(code, opts?)`
|
|
17
|
-
|
|
18
|
-
Set the response status code. Runs as an `after` interceptor.
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import { Post, SetStatus } from '@moostjs/event-http'
|
|
22
|
-
|
|
23
|
-
@Post('users')
|
|
24
|
-
@SetStatus(201)
|
|
25
|
-
create() { return { created: true } }
|
|
26
|
-
|
|
27
|
-
// Force override even if status was already set
|
|
28
|
-
@SetStatus(200, { force: true })
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### `@StatusRef()`
|
|
32
|
-
|
|
33
|
-
Dynamic status code control via a `{ value: number }` proxy object.
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
import { Get, StatusRef } from '@moostjs/event-http'
|
|
37
|
-
import type { TStatusRef } from '@moostjs/event-http'
|
|
38
|
-
|
|
39
|
-
@Get('process')
|
|
40
|
-
process(@StatusRef() status: TStatusRef) {
|
|
41
|
-
if (someCondition) {
|
|
42
|
-
status.value = 202
|
|
43
|
-
return { status: 'processing' }
|
|
44
|
-
}
|
|
45
|
-
return { status: 'done' } // default 200
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Works as a property decorator too:
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
@Injectable('FOR_EVENT')
|
|
53
|
-
@Controller()
|
|
54
|
-
class MyController {
|
|
55
|
-
@StatusRef()
|
|
56
|
-
status = 200 // initial value
|
|
57
|
-
|
|
58
|
-
@Get('test')
|
|
59
|
-
test() {
|
|
60
|
-
this.status = 201 // reactive — sets the response status
|
|
61
|
-
return 'created'
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### `@SetHeader(name, value, opts?)`
|
|
67
|
-
|
|
68
|
-
Set a response header. Runs as an `after` interceptor (and optionally on error).
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
import { Get, SetHeader } from '@moostjs/event-http'
|
|
72
|
-
|
|
73
|
-
@Get('test')
|
|
74
|
-
@SetHeader('x-powered-by', 'moost')
|
|
75
|
-
@SetHeader('cache-control', 'no-store')
|
|
76
|
-
test() { return 'ok' }
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Options:
|
|
80
|
-
|
|
81
|
-
| Option | Type | Description |
|
|
82
|
-
|--------|------|-------------|
|
|
83
|
-
| `force` | `boolean` | Override header even if already set |
|
|
84
|
-
| `status` | `number` | Only set when response has this status |
|
|
85
|
-
| `when` | `'always' \| 'error' \| 'ok'` | When to apply (default: success only) |
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
@SetHeader('content-type', 'text/plain', { status: 400 })
|
|
89
|
-
@SetHeader('x-request-id', 'abc', { when: 'always' })
|
|
90
|
-
@SetHeader('x-error', 'true', { when: 'error' })
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### `@HeaderRef(name)`
|
|
94
|
-
|
|
95
|
-
Dynamic header control via a `{ value: string | string[] | undefined }` proxy.
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
import { Get, HeaderRef } from '@moostjs/event-http'
|
|
99
|
-
import type { THeaderRef } from '@moostjs/event-http'
|
|
100
|
-
|
|
101
|
-
@Get('test')
|
|
102
|
-
test(@HeaderRef('x-custom') header: THeaderRef) {
|
|
103
|
-
header.value = `generated-${Date.now()}`
|
|
104
|
-
return 'ok'
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### `@SetCookie(name, value, attrs?)`
|
|
109
|
-
|
|
110
|
-
Set a response cookie. Only sets if the cookie hasn't already been set in the response.
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import { Get, SetCookie } from '@moostjs/event-http'
|
|
114
|
-
|
|
115
|
-
@Get('login')
|
|
116
|
-
@SetCookie('session', 'abc123', { maxAge: '1h', httpOnly: true })
|
|
117
|
-
login() { return { ok: true } }
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Cookie attributes (`TCookieAttributesInput`): `maxAge`, `expires`, `domain`, `path`, `secure`, `httpOnly`, `sameSite`, etc.
|
|
121
|
-
|
|
122
|
-
### `@CookieRef(name)`
|
|
123
|
-
|
|
124
|
-
Dynamic cookie value control.
|
|
125
|
-
|
|
126
|
-
```ts
|
|
127
|
-
import { Post, CookieRef } from '@moostjs/event-http'
|
|
128
|
-
import type { TCookieRef } from '@moostjs/event-http'
|
|
129
|
-
|
|
130
|
-
@Post('login')
|
|
131
|
-
login(@CookieRef('session') cookie: TCookieRef) {
|
|
132
|
-
cookie.value = generateToken()
|
|
133
|
-
return { ok: true }
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### `@CookieAttrsRef(name)`
|
|
138
|
-
|
|
139
|
-
Dynamic cookie attributes control.
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
import { Post, CookieAttrsRef } from '@moostjs/event-http'
|
|
143
|
-
import type { TCookieAttributes } from '@moostjs/event-http'
|
|
144
|
-
|
|
145
|
-
@Post('login')
|
|
146
|
-
login(@CookieAttrsRef('session') attrs: { value: TCookieAttributes }) {
|
|
147
|
-
attrs.value = { maxAge: '1h', httpOnly: true, secure: true }
|
|
148
|
-
return { ok: true }
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Body limits
|
|
153
|
-
|
|
154
|
-
Interceptor-based decorators for controlling request body parsing limits.
|
|
155
|
-
|
|
156
|
-
### `@BodySizeLimit(n)`
|
|
157
|
-
|
|
158
|
-
Limit the maximum inflated (decompressed) body size in bytes. Default: 10 MB.
|
|
159
|
-
|
|
160
|
-
```ts
|
|
161
|
-
import { Post, BodySizeLimit } from '@moostjs/event-http'
|
|
162
|
-
|
|
163
|
-
@Post('upload')
|
|
164
|
-
@BodySizeLimit(50 * 1024 * 1024) // 50 MB
|
|
165
|
-
upload() {}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### `@CompressedBodySizeLimit(n)`
|
|
169
|
-
|
|
170
|
-
Limit the maximum compressed body size in bytes. Default: 1 MB.
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
@Post('upload')
|
|
174
|
-
@CompressedBodySizeLimit(5 * 1024 * 1024) // 5 MB
|
|
175
|
-
upload() {}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### `@BodyReadTimeoutMs(n)`
|
|
179
|
-
|
|
180
|
-
Set the timeout for reading the request body in milliseconds. Default: 10 s.
|
|
181
|
-
|
|
182
|
-
```ts
|
|
183
|
-
@Post('upload')
|
|
184
|
-
@BodyReadTimeoutMs(30000) // 30 seconds
|
|
185
|
-
upload() {}
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### Global limit interceptors
|
|
189
|
-
|
|
190
|
-
For applying limits globally:
|
|
191
|
-
|
|
192
|
-
```ts
|
|
193
|
-
import { globalBodySizeLimit, globalCompressedBodySizeLimit, globalBodyReadTimeoutMs } from '@moostjs/event-http'
|
|
194
|
-
|
|
195
|
-
const app = new Moost()
|
|
196
|
-
app.applyGlobalInterceptors(
|
|
197
|
-
globalBodySizeLimit(20 * 1024 * 1024),
|
|
198
|
-
globalCompressedBodySizeLimit(2 * 1024 * 1024),
|
|
199
|
-
globalBodyReadTimeoutMs(15000),
|
|
200
|
-
)
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## Error handling
|
|
204
|
-
|
|
205
|
-
### `HttpError`
|
|
206
|
-
|
|
207
|
-
Throw to produce an HTTP error response:
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
import { HttpError } from '@moostjs/event-http'
|
|
211
|
-
|
|
212
|
-
throw new HttpError(404, 'Not Found')
|
|
213
|
-
throw new HttpError(403, 'Access denied')
|
|
214
|
-
throw new HttpError(422, {
|
|
215
|
-
message: 'Validation failed',
|
|
216
|
-
statusCode: 422,
|
|
217
|
-
errors: [
|
|
218
|
-
{ field: 'email', message: 'Invalid email format' },
|
|
219
|
-
],
|
|
220
|
-
})
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
Uncaught exceptions become HTTP 500 responses. The response format (JSON or HTML) adapts based on the `Accept` header.
|
|
224
|
-
|
|
225
|
-
## Common Patterns
|
|
226
|
-
|
|
227
|
-
### Pattern: CRUD with proper status codes
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
@Controller('items')
|
|
231
|
-
class ItemController {
|
|
232
|
-
@Get('')
|
|
233
|
-
list() { return items }
|
|
234
|
-
|
|
235
|
-
@Post('')
|
|
236
|
-
@SetStatus(201)
|
|
237
|
-
create(@Body() data: CreateItemDto) { return createItem(data) }
|
|
238
|
-
|
|
239
|
-
@Put(':id')
|
|
240
|
-
update(@Param('id') id: string, @Body() data: UpdateItemDto) {
|
|
241
|
-
return updateItem(id, data)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
@Delete(':id')
|
|
245
|
-
@SetStatus(204)
|
|
246
|
-
remove(@Param('id') id: string) { deleteItem(id) }
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### Pattern: Conditional response headers
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
@Get('data')
|
|
254
|
-
@SetHeader('x-cache', 'miss', { when: 'always' })
|
|
255
|
-
getData(@HeaderRef('x-cache') cache: THeaderRef) {
|
|
256
|
-
const cached = getFromCache()
|
|
257
|
-
if (cached) {
|
|
258
|
-
cache.value = 'hit'
|
|
259
|
-
return cached
|
|
260
|
-
}
|
|
261
|
-
return fetchFreshData()
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
## Types
|
|
266
|
-
|
|
267
|
-
```ts
|
|
268
|
-
export interface TStatusRef { value: number }
|
|
269
|
-
export interface THeaderRef { value: string | string[] | undefined }
|
|
270
|
-
export interface TCookieRef { value: string; attrs?: TCookieAttributes }
|
|
271
|
-
export type TCookieAttributes = Partial<TCookieAttributesRequired>
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
## Best Practices
|
|
275
|
-
|
|
276
|
-
- Use `@SetStatus` for fixed status codes (201 for creation, 204 for deletion)
|
|
277
|
-
- Use `@StatusRef` when the status depends on runtime logic
|
|
278
|
-
- Use `@SetHeader` for static headers, `@HeaderRef` for dynamic ones
|
|
279
|
-
- Apply body limits on upload endpoints to prevent abuse
|
|
280
|
-
- Use global limit interceptors for application-wide defaults
|
|
281
|
-
|
|
282
|
-
## Gotchas
|
|
283
|
-
|
|
284
|
-
- `@SetStatus` won't override a status already set (e.g., by an error) unless `{ force: true }` is passed
|
|
285
|
-
- `@SetCookie` won't overwrite a cookie already set in the response — this prevents accidental overwrites
|
|
286
|
-
- `@SetHeader` defaults to success-only (`after` interceptor); use `{ when: 'always' }` to include error responses
|
|
287
|
-
- Ref decorators use Proxy objects — the `.value` property is reactive, not a plain field
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
# Routing & handlers — @moostjs/event-http
|
|
2
|
-
|
|
3
|
-
> Defining HTTP routes, methods, parameters, wildcards, and handler return values.
|
|
4
|
-
|
|
5
|
-
## Concepts
|
|
6
|
-
|
|
7
|
-
Every HTTP endpoint is defined by a method decorator (`@Get`, `@Post`, etc.) on a controller method. The decorator specifies the HTTP method and optional path. Route parameters (`:param`), wildcards (`*`), and regex constraints provide flexible URL matching. Controllers group related handlers under a shared prefix.
|
|
8
|
-
|
|
9
|
-
## API Reference
|
|
10
|
-
|
|
11
|
-
### HTTP method decorators
|
|
12
|
-
|
|
13
|
-
All imported from `@moostjs/event-http`:
|
|
14
|
-
|
|
15
|
-
| Decorator | HTTP Method | Usage |
|
|
16
|
-
|-----------|-------------|-------|
|
|
17
|
-
| `@Get(path?)` | GET | `@Get('users/:id')` |
|
|
18
|
-
| `@Post(path?)` | POST | `@Post('users')` |
|
|
19
|
-
| `@Put(path?)` | PUT | `@Put('users/:id')` |
|
|
20
|
-
| `@Delete(path?)` | DELETE | `@Delete('users/:id')` |
|
|
21
|
-
| `@Patch(path?)` | PATCH | `@Patch('users/:id')` |
|
|
22
|
-
| `@All(path?)` | All methods | `@All('proxy/*')` |
|
|
23
|
-
| `@HttpMethod(method, path?)` | Any method | `@HttpMethod('OPTIONS', '')` |
|
|
24
|
-
| `@Upgrade(path?)` | UPGRADE | `@Upgrade('/ws')` |
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
import { Get, Post, Put, Delete, Patch, All, HttpMethod, Upgrade } from '@moostjs/event-http'
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### `@HttpMethod(method, path?)`
|
|
31
|
-
|
|
32
|
-
The base decorator — all convenience decorators (`@Get`, `@Post`, etc.) call this internally.
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
@HttpMethod('HEAD', 'health')
|
|
36
|
-
healthCheck() { /* HEAD /health */ }
|
|
37
|
-
|
|
38
|
-
@HttpMethod('OPTIONS', '')
|
|
39
|
-
cors() { /* OPTIONS / */ }
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Valid methods: `'GET'`, `'PUT'`, `'POST'`, `'PATCH'`, `'DELETE'`, `'HEAD'`, `'OPTIONS'`, `'UPGRADE'`, `'*'`
|
|
43
|
-
|
|
44
|
-
### `@Upgrade(path?)`
|
|
45
|
-
|
|
46
|
-
Registers an UPGRADE route for WebSocket handshakes. Use with `@moostjs/event-ws`:
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
@Upgrade('/ws')
|
|
50
|
-
handleUpgrade(ws: WooksWs) {
|
|
51
|
-
return ws.upgrade()
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Route parameters — `@Param` and `@Params`
|
|
56
|
-
|
|
57
|
-
Imported from `moost` (not from `@moostjs/event-http`):
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
import { Controller, Param, Params } from 'moost'
|
|
61
|
-
import { Get } from '@moostjs/event-http'
|
|
62
|
-
|
|
63
|
-
@Controller('users')
|
|
64
|
-
class UserController {
|
|
65
|
-
@Get(':id')
|
|
66
|
-
find(@Param('id') id: string) {
|
|
67
|
-
return { id } // GET /users/123
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
@Get(':type/:type/:id')
|
|
71
|
-
getAsset(@Params() params: { type: string[], id: string }) {
|
|
72
|
-
return params
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Common Patterns
|
|
78
|
-
|
|
79
|
-
### Pattern: Path defaults
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
@Get() // path = method name, e.g. GET /getUsers
|
|
83
|
-
getUsers() {}
|
|
84
|
-
|
|
85
|
-
@Get('') // path = controller root, e.g. GET /
|
|
86
|
-
root() {}
|
|
87
|
-
|
|
88
|
-
@Get('list') // explicit path, e.g. GET /list
|
|
89
|
-
listItems() {}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Pattern: Multiple parameters
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
// Slash-separated: /flights/SFO/LAX
|
|
96
|
-
@Get('flights/:from/:to')
|
|
97
|
-
getFlight(@Param('from') from: string, @Param('to') to: string) {}
|
|
98
|
-
|
|
99
|
-
// Hyphen-separated: /dates/2024-01-15
|
|
100
|
-
@Get('dates/:year-:month-:day')
|
|
101
|
-
getDate(
|
|
102
|
-
@Param('year') year: string,
|
|
103
|
-
@Param('month') month: string,
|
|
104
|
-
@Param('day') day: string,
|
|
105
|
-
) {}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Pattern: Regex-constrained parameters
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
// Only matches two-digit hours and minutes: /time/09h30m
|
|
112
|
-
@Get('time/:hours(\\d{2})h:minutes(\\d{2})m')
|
|
113
|
-
getTime(@Param('hours') hours: string, @Param('minutes') minutes: string) {}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### Pattern: Repeated parameters (arrays)
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
// /rgb/255/128/0 -> color = ['255', '128', '0']
|
|
120
|
-
@Get('rgb/:color/:color/:color')
|
|
121
|
-
getRgb(@Param('color') color: string[]) {}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Pattern: Wildcards
|
|
125
|
-
|
|
126
|
-
```ts
|
|
127
|
-
@Controller('static')
|
|
128
|
-
class StaticController {
|
|
129
|
-
@Get('*')
|
|
130
|
-
handleAll(@Param('*') path: string) {}
|
|
131
|
-
|
|
132
|
-
@Get('*.js')
|
|
133
|
-
handleJS(@Param('*') name: string) {}
|
|
134
|
-
|
|
135
|
-
// Multiple wildcards -> array
|
|
136
|
-
@Get('*/test/*')
|
|
137
|
-
handleTest(@Param('*') paths: string[]) {}
|
|
138
|
-
|
|
139
|
-
// Regex on wildcard: only digits
|
|
140
|
-
@Get('*(\\d+)')
|
|
141
|
-
handleNumbers(@Param('*') path: string) {}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Pattern: Controller prefix
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
import { Controller } from 'moost'
|
|
149
|
-
|
|
150
|
-
@Controller('api/v1/users')
|
|
151
|
-
class UserController {
|
|
152
|
-
@Get('') // GET /api/v1/users
|
|
153
|
-
list() {}
|
|
154
|
-
|
|
155
|
-
@Get(':id') // GET /api/v1/users/123
|
|
156
|
-
find() {}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Pattern: Nested controllers
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
import { Controller, ImportController } from 'moost'
|
|
164
|
-
|
|
165
|
-
@Controller('api')
|
|
166
|
-
class ApiController {
|
|
167
|
-
@ImportController(() => UserController)
|
|
168
|
-
users!: UserController
|
|
169
|
-
|
|
170
|
-
@ImportController(() => ProductController)
|
|
171
|
-
products!: ProductController
|
|
172
|
-
}
|
|
173
|
-
// Routes: GET /api/users/..., GET /api/products/...
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## Handler return values
|
|
177
|
-
|
|
178
|
-
Whatever the handler returns becomes the response body:
|
|
179
|
-
|
|
180
|
-
| Return type | Content-Type |
|
|
181
|
-
|-------------|--------------|
|
|
182
|
-
| `string` | `text/plain` |
|
|
183
|
-
| object / array | `application/json` |
|
|
184
|
-
| `ReadableStream` | streamed |
|
|
185
|
-
| fetch `Response` | forwarded as-is |
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
@Get('text')
|
|
189
|
-
getText() { return 'Hello!' } // text/plain
|
|
190
|
-
|
|
191
|
-
@Get('json')
|
|
192
|
-
getJson() { return { message: 'Hi' } } // application/json
|
|
193
|
-
|
|
194
|
-
@Get('data')
|
|
195
|
-
async getData() { return await fetchFromDb() } // async works
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
## Best Practices
|
|
199
|
-
|
|
200
|
-
- Use `@Get('')` (empty string) for the controller root path, not `@Get()` (which uses the method name)
|
|
201
|
-
- Keep controller prefixes as REST resource names: `@Controller('users')`, `@Controller('products')`
|
|
202
|
-
- Use `@Param` for individual route parameters, `@Params` when you need the full params object
|
|
203
|
-
- Prefer convenience decorators (`@Get`, `@Post`) over `@HttpMethod` for standard methods
|
|
204
|
-
|
|
205
|
-
## Gotchas
|
|
206
|
-
|
|
207
|
-
- `@Get()` without arguments uses the **method name** as the path — this is rarely what you want
|
|
208
|
-
- Route parameters are always strings — use pipes to transform to numbers/booleans
|
|
209
|
-
- An explicit double slash `//` at the end of a path forces the URL to end with a trailing slash
|
|
210
|
-
- Query parameters are not part of the route path — use `@Query()` to extract them
|