@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.
@@ -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.