@moostjs/event-http 0.6.5 → 0.6.7

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 CHANGED
@@ -662,6 +662,7 @@ const CONTEXT_TYPE = "HTTP";
662
662
  raw.on("end", unscope);
663
663
  } },
664
664
  targetPath,
665
+ controllerPrefix: opts.prefix,
665
666
  handlerType: handler.type
666
667
  });
667
668
  const routerBinding = handler.method === "UPGRADE" ? this.httpApp.upgrade(targetPath, fn) : this.httpApp.on(handler.method, targetPath, fn);
@@ -694,7 +695,7 @@ const CONTEXT_TYPE = "HTTP";
694
695
  image: "https://moost.org/moost-full-logo.svg",
695
696
  link: "https://moost.org/",
696
697
  poweredBy: "moostjs",
697
- version: "0.6.4"
698
+ version: "0.6.6"
698
699
  });
699
700
  if (httpApp && httpApp instanceof __wooksjs_event_http.WooksHttp) this.httpApp = httpApp;
700
701
  else if (httpApp) this.httpApp = (0, __wooksjs_event_http.createHttpApp)({
package/dist/index.mjs CHANGED
@@ -639,6 +639,7 @@ const CONTEXT_TYPE = "HTTP";
639
639
  raw.on("end", unscope);
640
640
  } },
641
641
  targetPath,
642
+ controllerPrefix: opts.prefix,
642
643
  handlerType: handler.type
643
644
  });
644
645
  const routerBinding = handler.method === "UPGRADE" ? this.httpApp.upgrade(targetPath, fn) : this.httpApp.on(handler.method, targetPath, fn);
@@ -671,7 +672,7 @@ const CONTEXT_TYPE = "HTTP";
671
672
  image: "https://moost.org/moost-full-logo.svg",
672
673
  link: "https://moost.org/",
673
674
  poweredBy: "moostjs",
674
- version: "0.6.4"
675
+ version: "0.6.6"
675
676
  });
676
677
  if (httpApp && httpApp instanceof WooksHttp) this.httpApp = httpApp;
677
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.5",
3
+ "version": "0.6.7",
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.8",
47
- "@wooksjs/http-body": "^0.7.8"
41
+ "@wooksjs/event-http": "^0.7.9",
42
+ "@wooksjs/http-body": "^0.7.9"
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.8",
55
- "moost": "^0.6.5"
49
+ "@wooksjs/event-core": "^0.7.9",
50
+ "moost": "^0.6.7"
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
  }
@@ -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