@wooksjs/event-http 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/index.cjs +132 -25
- package/dist/index.d.ts +110 -12
- package/dist/index.mjs +132 -25
- package/package.json +45 -37
- package/scripts/setup-skills.js +77 -0
- package/skills/wooksjs-event-http/SKILL.md +37 -0
- package/skills/wooksjs-event-http/addons.md +307 -0
- package/skills/wooksjs-event-http/core.md +297 -0
- package/skills/wooksjs-event-http/error-handling.md +253 -0
- package/skills/wooksjs-event-http/event-core.md +562 -0
- package/skills/wooksjs-event-http/request.md +220 -0
- package/skills/wooksjs-event-http/response.md +336 -0
- package/skills/wooksjs-event-http/routing.md +412 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# Routing — @wooksjs/event-http
|
|
2
|
+
|
|
3
|
+
> Covers route registration, the full route syntax (params, wildcards, regex constraints, optional segments), HTTP methods, handler return values, router configuration, and path builders. Wooks uses `@prostojs/router` — a high-performance radix-tree router.
|
|
4
|
+
|
|
5
|
+
## Route Registration
|
|
6
|
+
|
|
7
|
+
### Method shortcuts
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createHttpApp } from '@wooksjs/event-http'
|
|
11
|
+
|
|
12
|
+
const app = createHttpApp()
|
|
13
|
+
|
|
14
|
+
app.get('/users', () => { /* GET /users */ })
|
|
15
|
+
app.post('/users', () => { /* POST /users */ })
|
|
16
|
+
app.put('/users/:id', () => { /* PUT /users/:id */ })
|
|
17
|
+
app.patch('/users/:id', () => { /* PATCH /users/:id */ })
|
|
18
|
+
app.delete('/users/:id', () => { /* DELETE /users/:id */ })
|
|
19
|
+
app.head('/users', () => { /* HEAD /users */ })
|
|
20
|
+
app.options('/users', () => { /* OPTIONS /users */ })
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Catch-all method
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
app.all('/health', () => 'OK') // matches any HTTP method
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Generic `on()` method
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
app.on('GET', '/hello', () => 'Hello!')
|
|
33
|
+
app.on('CUSTOM', '/rpc', () => 'Custom method')
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Route Syntax
|
|
37
|
+
|
|
38
|
+
### Static routes
|
|
39
|
+
|
|
40
|
+
Exact path matching with no parameters:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
app.get('/api/status', () => 'ok')
|
|
44
|
+
app.get('/about/team', () => 'team page')
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Named parameters (`:paramName`)
|
|
48
|
+
|
|
49
|
+
Captures a single path segment:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { useRouteParams } from '@wooksjs/event-http'
|
|
53
|
+
|
|
54
|
+
app.get('/users/:id', () => {
|
|
55
|
+
const { get } = useRouteParams<{ id: string }>()
|
|
56
|
+
return { userId: get('id') }
|
|
57
|
+
})
|
|
58
|
+
// GET /users/42 → { userId: '42' }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Multiple parameters
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
app.get('/users/:userId/posts/:postId', () => {
|
|
65
|
+
const { params } = useRouteParams<{ userId: string; postId: string }>()
|
|
66
|
+
return params
|
|
67
|
+
})
|
|
68
|
+
// GET /users/42/posts/7 → { userId: '42', postId: '7' }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Hyphen-separated parameters
|
|
72
|
+
|
|
73
|
+
Parameters can be separated by hyphens (or other literal characters) within a single segment:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
app.get('/flights/:from-:to', () => {
|
|
77
|
+
const { get } = useRouteParams<{ from: string; to: string }>()
|
|
78
|
+
return { from: get('from'), to: get('to') }
|
|
79
|
+
})
|
|
80
|
+
// GET /flights/NYC-LAX → { from: 'NYC', to: 'LAX' }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Regex-constrained parameters
|
|
84
|
+
|
|
85
|
+
Append a regex pattern in parentheses to restrict what a parameter matches:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// Only match numeric IDs
|
|
89
|
+
app.get('/users/:id(\\d+)', () => {
|
|
90
|
+
const { get } = useRouteParams<{ id: string }>()
|
|
91
|
+
return { id: get('id') }
|
|
92
|
+
})
|
|
93
|
+
// GET /users/42 → matches, { id: '42' }
|
|
94
|
+
// GET /users/alice → does NOT match (404)
|
|
95
|
+
|
|
96
|
+
// Complex: time format
|
|
97
|
+
app.get('/schedule/:hours(\\d{2})h:minutes(\\d{2})m', () => {
|
|
98
|
+
const { get } = useRouteParams<{ hours: string; minutes: string }>()
|
|
99
|
+
return { hours: get('hours'), minutes: get('minutes') }
|
|
100
|
+
})
|
|
101
|
+
// GET /schedule/09h30m → { hours: '09', minutes: '30' }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Repeated parameters (array capture)
|
|
105
|
+
|
|
106
|
+
Using the same parameter name multiple times captures values as an array:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
app.get('/tags/:tag/:tag/:tag', () => {
|
|
110
|
+
const { get } = useRouteParams<{ tag: string[] }>()
|
|
111
|
+
return { tags: get('tag') }
|
|
112
|
+
})
|
|
113
|
+
// GET /tags/js/ts/rust → { tags: ['js', 'ts', 'rust'] }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Wildcards (`*`)
|
|
117
|
+
|
|
118
|
+
Captures arbitrary path segments (including slashes):
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// Prefix wildcard — capture everything after /files/
|
|
122
|
+
app.get('/files/*', () => {
|
|
123
|
+
const { get } = useRouteParams<{ '*': string }>()
|
|
124
|
+
return { path: get('*') }
|
|
125
|
+
})
|
|
126
|
+
// GET /files/docs/readme.txt → { path: 'docs/readme.txt' }
|
|
127
|
+
|
|
128
|
+
// Suffix wildcard — match specific extensions
|
|
129
|
+
app.get('/assets/*.js', () => {
|
|
130
|
+
const { get } = useRouteParams<{ '*': string }>()
|
|
131
|
+
return { file: get('*') }
|
|
132
|
+
})
|
|
133
|
+
// GET /assets/app.bundle.js → { file: 'app.bundle' }
|
|
134
|
+
|
|
135
|
+
// Multiple wildcards
|
|
136
|
+
app.get('/api/*/v2/*', () => {
|
|
137
|
+
// Each * is captured independently
|
|
138
|
+
const { params } = useRouteParams()
|
|
139
|
+
return params
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Regex-constrained wildcard
|
|
143
|
+
app.get('/page/*(\\d+)', () => {
|
|
144
|
+
const { get } = useRouteParams<{ '*': string }>()
|
|
145
|
+
return { page: get('*') }
|
|
146
|
+
})
|
|
147
|
+
// GET /page/42 → matches
|
|
148
|
+
// GET /page/abc → does NOT match
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Optional parameters (`?`)
|
|
152
|
+
|
|
153
|
+
Append `?` to make a parameter optional. Optional params must be at the end of the path:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
app.get('/users/:id/:tab?', () => {
|
|
157
|
+
const { get } = useRouteParams<{ id: string; tab?: string }>()
|
|
158
|
+
return { id: get('id'), tab: get('tab') || 'profile' }
|
|
159
|
+
})
|
|
160
|
+
// GET /users/42 → { id: '42', tab: 'profile' }
|
|
161
|
+
// GET /users/42/posts → { id: '42', tab: 'posts' }
|
|
162
|
+
|
|
163
|
+
// Multiple optional params
|
|
164
|
+
app.get('/archive/:year/:month?/:day?', () => {
|
|
165
|
+
const { params } = useRouteParams<{ year: string; month?: string; day?: string }>()
|
|
166
|
+
return params
|
|
167
|
+
})
|
|
168
|
+
// GET /archive/2024 → { year: '2024' }
|
|
169
|
+
// GET /archive/2024/03 → { year: '2024', month: '03' }
|
|
170
|
+
// GET /archive/2024/03/15 → { year: '2024', month: '03', day: '15' }
|
|
171
|
+
|
|
172
|
+
// Optional wildcard
|
|
173
|
+
app.get('/docs/:*?', () => {
|
|
174
|
+
const { get } = useRouteParams<{ '*'?: string }>()
|
|
175
|
+
return { path: get('*') || 'index' }
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Escaping colons
|
|
180
|
+
|
|
181
|
+
Use `\\:` for literal colons in the path (not parameter delimiters):
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
app.get('/time/\\:hours\\::minutes', () => { /* matches /time/:hours:30 literally */ })
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Accessing Route Parameters
|
|
188
|
+
|
|
189
|
+
### `useRouteParams<T>()`
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { useRouteParams } from '@wooksjs/event-http'
|
|
193
|
+
|
|
194
|
+
const { params, get } = useRouteParams<{ id: string }>()
|
|
195
|
+
|
|
196
|
+
params // full params object: { id: '123' }
|
|
197
|
+
get('id') // single param: '123'
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- Parameters are always `string` or `string[]` (for repeated params).
|
|
201
|
+
- Cast numerics yourself: `Number(get('id'))`.
|
|
202
|
+
- The generic `<T>` gives type-safe access via `get()`.
|
|
203
|
+
|
|
204
|
+
## Path Builders
|
|
205
|
+
|
|
206
|
+
Route registration returns a path handle with a `getPath` builder — useful for generating URLs from parameter objects:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const userRoute = app.get('/users/:id', handler)
|
|
210
|
+
userRoute.getPath({ id: '42' })
|
|
211
|
+
// → '/users/42'
|
|
212
|
+
|
|
213
|
+
const fileRoute = app.get('/files/*', handler)
|
|
214
|
+
fileRoute.getPath({ '*': 'docs/readme.txt' })
|
|
215
|
+
// → '/files/docs/readme.txt'
|
|
216
|
+
|
|
217
|
+
const tagRoute = app.get('/tags/:tag/:tag/:tag', handler)
|
|
218
|
+
tagRoute.getPath({ tag: ['js', 'ts', 'rust'] })
|
|
219
|
+
// → '/tags/js/ts/rust'
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Handler Return Values
|
|
223
|
+
|
|
224
|
+
Handlers return the response body directly. The framework automatically determines the content type and status code:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
// String → text/plain
|
|
228
|
+
app.get('/text', () => 'Hello')
|
|
229
|
+
|
|
230
|
+
// Object/Array → application/json (auto-serialized)
|
|
231
|
+
app.get('/json', () => ({ message: 'Hello' }))
|
|
232
|
+
|
|
233
|
+
// Number → text/plain
|
|
234
|
+
app.get('/number', () => 42)
|
|
235
|
+
|
|
236
|
+
// Boolean → text/plain
|
|
237
|
+
app.get('/bool', () => true)
|
|
238
|
+
|
|
239
|
+
// undefined → 204 No Content
|
|
240
|
+
app.get('/empty', () => {})
|
|
241
|
+
|
|
242
|
+
// Readable stream → streamed response
|
|
243
|
+
app.get('/stream', () => createReadStream('/path/to/file'))
|
|
244
|
+
|
|
245
|
+
// Fetch Response → proxied response
|
|
246
|
+
app.get('/proxy', async () => {
|
|
247
|
+
return await fetch('https://api.example.com/data')
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Default Status Codes (when not explicitly set)
|
|
252
|
+
|
|
253
|
+
| Method | Default status |
|
|
254
|
+
|---------|---------------|
|
|
255
|
+
| GET | 200 OK |
|
|
256
|
+
| POST | 201 Created |
|
|
257
|
+
| PUT | 201 Created |
|
|
258
|
+
| PATCH | 202 Accepted |
|
|
259
|
+
| DELETE | 202 Accepted |
|
|
260
|
+
| (empty body) | 204 No Content |
|
|
261
|
+
|
|
262
|
+
## Async Handlers
|
|
263
|
+
|
|
264
|
+
Handlers can be async. The framework awaits the returned promise:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
app.get('/data', async () => {
|
|
268
|
+
const data = await fetchFromDatabase()
|
|
269
|
+
return data
|
|
270
|
+
})
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Router Configuration
|
|
274
|
+
|
|
275
|
+
Pass router options when creating the app:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
const app = createHttpApp({
|
|
279
|
+
router: {
|
|
280
|
+
ignoreTrailingSlash: true, // /users and /users/ match the same route
|
|
281
|
+
ignoreCase: true, // /Users and /users match the same route
|
|
282
|
+
cacheLimit: 1000, // max cached parsed route lookups (default: 50)
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
| Option | Default | Description |
|
|
288
|
+
|--------|---------|-------------|
|
|
289
|
+
| `ignoreTrailingSlash` | `false` | Treat `/path` and `/path/` as the same route |
|
|
290
|
+
| `ignoreCase` | `false` | Case-insensitive route matching |
|
|
291
|
+
| `cacheLimit` | `50` | Max number of parsed URL-to-route mappings to cache |
|
|
292
|
+
|
|
293
|
+
## Custom 404 Handler
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
const app = createHttpApp({
|
|
297
|
+
onNotFound: () => {
|
|
298
|
+
const { url, method } = useRequest()
|
|
299
|
+
throw new HttpError(404, `${method} ${url} not found`)
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Sharing Router Between Adapters
|
|
305
|
+
|
|
306
|
+
Multiple adapters can share the same Wooks router instance:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { Wooks } from 'wooks'
|
|
310
|
+
import { createHttpApp } from '@wooksjs/event-http'
|
|
311
|
+
|
|
312
|
+
const wooks = new Wooks()
|
|
313
|
+
const app1 = createHttpApp({}, wooks)
|
|
314
|
+
const app2 = createHttpApp({}, wooks) // shares the same routes
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Or share via another adapter:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
const app1 = createHttpApp()
|
|
321
|
+
const app2 = createHttpApp({}, app1) // shares app1's router
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Common Patterns
|
|
325
|
+
|
|
326
|
+
### Pattern: RESTful Resource
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
const app = createHttpApp()
|
|
330
|
+
|
|
331
|
+
// List
|
|
332
|
+
app.get('/api/users', async () => {
|
|
333
|
+
return await db.listUsers()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// Get by ID
|
|
337
|
+
app.get('/api/users/:id(\\d+)', async () => {
|
|
338
|
+
const { get } = useRouteParams<{ id: string }>()
|
|
339
|
+
const user = await db.findUser(Number(get('id')))
|
|
340
|
+
if (!user) throw new HttpError(404, 'User not found')
|
|
341
|
+
return user
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// Create
|
|
345
|
+
app.post('/api/users', async () => {
|
|
346
|
+
const { parseBody } = useBody()
|
|
347
|
+
const data = await parseBody<{ name: string; email: string }>()
|
|
348
|
+
return await db.createUser(data)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// Update
|
|
352
|
+
app.patch('/api/users/:id(\\d+)', async () => {
|
|
353
|
+
const { get } = useRouteParams<{ id: string }>()
|
|
354
|
+
const { parseBody } = useBody()
|
|
355
|
+
const data = await parseBody<Partial<User>>()
|
|
356
|
+
return await db.updateUser(Number(get('id')), data)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// Delete
|
|
360
|
+
app.delete('/api/users/:id(\\d+)', async () => {
|
|
361
|
+
const { get } = useRouteParams<{ id: string }>()
|
|
362
|
+
await db.deleteUser(Number(get('id')))
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Pattern: Static File Server with API
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
// API routes (more specific) are matched first
|
|
370
|
+
app.get('/api/status', () => ({ status: 'ok' }))
|
|
371
|
+
app.get('/api/users/:id', () => { /* ... */ })
|
|
372
|
+
|
|
373
|
+
// Static catch-all
|
|
374
|
+
app.get('/*', () => {
|
|
375
|
+
const { get } = useRouteParams<{ '*': string }>()
|
|
376
|
+
return serveFile(get('*') || 'index.html', { baseDir: './public' })
|
|
377
|
+
})
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Pattern: Versioned API
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
app.get('/api/v1/users', () => handleV1Users())
|
|
384
|
+
app.get('/api/v2/users', () => handleV2Users())
|
|
385
|
+
|
|
386
|
+
// Or with a parameter
|
|
387
|
+
app.get('/api/:version(v\\d+)/users', () => {
|
|
388
|
+
const { get } = useRouteParams<{ version: string }>()
|
|
389
|
+
if (get('version') === 'v1') return handleV1Users()
|
|
390
|
+
return handleV2Users()
|
|
391
|
+
})
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Best Practices
|
|
395
|
+
|
|
396
|
+
- **Use typed generics on `useRouteParams<T>()`** for type-safe parameter access.
|
|
397
|
+
- **Use regex constraints** to validate params at the routing level: `:id(\\d+)` prevents non-numeric IDs from even reaching your handler.
|
|
398
|
+
- **Prefer method shortcuts** (`app.get()`, `app.post()`) over `app.on()` for readability.
|
|
399
|
+
- **Return objects directly** — the framework serializes to JSON automatically.
|
|
400
|
+
- **Use `app.all()`** for routes that respond to any HTTP method (health checks, CORS preflight fallbacks).
|
|
401
|
+
- **Use path builders** when you need to generate URLs programmatically (e.g., for `Location` headers in redirects).
|
|
402
|
+
- **Avoid optional parameters when possible** — they reduce routing performance since the router must check multiple path variants. Prefer explicit routes.
|
|
403
|
+
|
|
404
|
+
## Gotchas
|
|
405
|
+
|
|
406
|
+
- **Route paths should not include query strings** — use `useSearchParams()` to access query parameters.
|
|
407
|
+
- **Parameters are always strings** — cast numerics yourself: `Number(get('id'))`.
|
|
408
|
+
- **Wildcard `*` captures everything** after its position, including slashes — `'/files/*'` matches `/files/`, `/files/a`, `/files/a/b/c`.
|
|
409
|
+
- **Optional params must be at the end** of the path. `/users/:id?/posts` is invalid.
|
|
410
|
+
- **Regex patterns use double backslashes** in string literals: `':id(\\d+)'` not `':id(\d+)'`.
|
|
411
|
+
- **URL-encoded characters are handled correctly** — the router decodes `%20`, `%2F`, etc. before matching. You get the decoded values in params.
|
|
412
|
+
- **Route precedence**: static segments match before parametric, parametric before wildcard. More specific routes always win.
|