remix 3.0.0-beta.0 → 3.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/fetch-router.d.ts +7 -0
  2. package/dist/fetch-router.d.ts.map +1 -1
  3. package/dist/node-tsx/load-module.d.ts +2 -0
  4. package/dist/node-tsx/load-module.d.ts.map +1 -0
  5. package/dist/node-tsx/load-module.js +2 -0
  6. package/dist/node-tsx.d.ts +3 -0
  7. package/dist/node-tsx.d.ts.map +1 -0
  8. package/{src/node-serve.ts → dist/node-tsx.js} +2 -1
  9. package/dist/render-middleware.d.ts +2 -0
  10. package/dist/render-middleware.d.ts.map +1 -0
  11. package/dist/render-middleware.js +2 -0
  12. package/dist/route-pattern/href.d.ts +2 -0
  13. package/dist/route-pattern/href.d.ts.map +1 -0
  14. package/dist/route-pattern/href.js +2 -0
  15. package/dist/route-pattern/join.d.ts +2 -0
  16. package/dist/route-pattern/join.d.ts.map +1 -0
  17. package/dist/route-pattern/join.js +2 -0
  18. package/dist/route-pattern/match.d.ts +2 -0
  19. package/dist/route-pattern/match.d.ts.map +1 -0
  20. package/dist/route-pattern/match.js +2 -0
  21. package/package.json +158 -44
  22. package/src/assert/README.md +109 -0
  23. package/src/assets/README.md +539 -0
  24. package/src/async-context-middleware/README.md +100 -0
  25. package/src/auth/README.md +445 -0
  26. package/src/auth-middleware/README.md +246 -0
  27. package/src/cli/README.md +78 -0
  28. package/src/compression-middleware/README.md +176 -0
  29. package/src/cookie/README.md +106 -0
  30. package/src/cop-middleware/README.md +117 -0
  31. package/src/cors-middleware/README.md +174 -0
  32. package/src/csrf-middleware/README.md +99 -0
  33. package/src/data-schema/README.md +422 -0
  34. package/src/data-table/README.md +552 -0
  35. package/src/data-table-mysql/README.md +97 -0
  36. package/src/data-table-postgres/README.md +74 -0
  37. package/src/data-table-sqlite/README.md +84 -0
  38. package/src/fetch-proxy/README.md +46 -0
  39. package/src/fetch-router/README.md +902 -0
  40. package/src/fetch-router.ts +7 -0
  41. package/src/file-storage/README.md +57 -0
  42. package/src/file-storage-s3/README.md +47 -0
  43. package/src/form-data-middleware/README.md +109 -0
  44. package/src/form-data-parser/README.md +160 -0
  45. package/src/fs/README.md +60 -0
  46. package/src/headers/README.md +629 -0
  47. package/src/html-template/README.md +101 -0
  48. package/src/lazy-file/README.md +109 -0
  49. package/src/logger-middleware/README.md +132 -0
  50. package/src/method-override-middleware/README.md +71 -0
  51. package/src/mime/README.md +110 -0
  52. package/src/multipart-parser/README.md +241 -0
  53. package/src/node-fetch-server/README.md +352 -0
  54. package/src/node-tsx/README.md +79 -0
  55. package/src/node-tsx/load-module.ts +2 -0
  56. package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
  57. package/src/render-middleware/README.md +99 -0
  58. package/src/render-middleware.ts +2 -0
  59. package/src/route-pattern/README.md +291 -0
  60. package/src/route-pattern/href.ts +2 -0
  61. package/src/route-pattern/join.ts +2 -0
  62. package/src/route-pattern/match.ts +2 -0
  63. package/src/session/README.md +171 -0
  64. package/src/session-middleware/README.md +109 -0
  65. package/src/session-storage-memcache/README.md +37 -0
  66. package/src/session-storage-redis/README.md +37 -0
  67. package/src/static-middleware/README.md +89 -0
  68. package/src/tar-parser/README.md +74 -0
  69. package/src/terminal/README.md +92 -0
  70. package/src/test/README.md +430 -0
  71. package/src/ui/README.md +219 -0
  72. package/src/ui/accordion/README.md +166 -0
  73. package/src/ui/anchor/README.md +153 -0
  74. package/src/ui/animation/README.md +316 -0
  75. package/src/ui/breadcrumbs/README.md +55 -0
  76. package/src/ui/button/README.md +44 -0
  77. package/src/ui/combobox/README.md +145 -0
  78. package/src/ui/glyph/README.md +72 -0
  79. package/src/ui/listbox/README.md +115 -0
  80. package/src/ui/menu/README.md +96 -0
  81. package/src/ui/popover/README.md +122 -0
  82. package/src/ui/scroll-lock/README.md +33 -0
  83. package/src/ui/select/README.md +107 -0
  84. package/src/ui/server/README.md +90 -0
  85. package/src/ui/test/README.md +107 -0
  86. package/src/ui/theme/README.md +103 -0
  87. package/dist/node-serve.d.ts +0 -2
  88. package/dist/node-serve.d.ts.map +0 -1
@@ -0,0 +1,902 @@
1
+ # fetch-router
2
+
3
+ A minimal, composable router built on the [web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and [`route-pattern`](https://github.com/remix-run/remix/tree/main/packages/route-pattern). Use it to define typed route maps, run middleware, and share request-scoped context across APIs, web services, and server-rendered applications.
4
+
5
+ ## Features
6
+
7
+ - **Fetch API**: Built on standard web APIs that work everywhere - Node.js, Bun, Deno, Cloudflare Workers, and browsers
8
+ - **Type-Safe Routing**: Leverage TypeScript for compile-time route validation and parameter inference
9
+ - **Typed Request Context**: Carry request-scoped context through routers, controllers, and actions
10
+ - **Declarative Route Maps**: Define your route structure upfront with type-safe route names and request methods
11
+ - **Flexible Middleware**: Apply middleware globally, per-route, or to controllers
12
+ - **Easy Testing**: Use standard `fetch()` to test your routes - no special test harness required
13
+
14
+ ## Installation
15
+
16
+ ```sh
17
+ npm i remix
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ The main purpose of the router is to map incoming requests to request handlers and middleware. The router uses the `fetch()` API to accept a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
23
+
24
+ Import route definition helpers (`route`, `form`, `resource`, `resources`, etc.) from `remix/routes` and runtime APIs (`createRouter`, `Middleware`, etc.) from `remix/router`.
25
+
26
+ The example below is a small site with a home page, an "about" page, and a blog.
27
+
28
+ ```ts
29
+ import { route } from 'remix/routes'
30
+ import { createRouter } from 'remix/router'
31
+ import { logger } from 'remix/middleware/logger'
32
+
33
+ // `route()` creates a "route map" that organizes routes by name. The keys
34
+ // of the map may be any name, and may be nested to group related routes.
35
+ let routes = route({
36
+ home: '/',
37
+ about: '/about',
38
+ blog: {
39
+ index: '/blog',
40
+ show: '/blog/:slug',
41
+ },
42
+ })
43
+
44
+ let router = createRouter({
45
+ // Middleware is used to run code before and/or after actions run.
46
+ // In this case, the `logger()` middleware logs the request to the console.
47
+ middleware: [logger()],
48
+ })
49
+
50
+ // Map a controller that supplies actions for the root routes.
51
+ // A controller is a plain object with an `actions` property that
52
+ // matches the direct route leaves in a route map.
53
+ router.map(routes, {
54
+ actions: {
55
+ home() {
56
+ return new Response('Home')
57
+ },
58
+ about() {
59
+ return new Response('About')
60
+ },
61
+ },
62
+ })
63
+
64
+ // Map another controller that supplies actions for the blog routes.
65
+ router.map(routes.blog, {
66
+ actions: {
67
+ index() {
68
+ return new Response('Blog')
69
+ },
70
+ show({ params }) {
71
+ // params is a type-safe object with the parameters from the route pattern
72
+ return new Response(`Post ${params.slug}`)
73
+ },
74
+ },
75
+ })
76
+
77
+ let response = await router.fetch('https://remix.run/blog/hello-remix')
78
+ console.log(await response.text()) // "Post hello-remix"
79
+ ```
80
+
81
+ The route map is an object of the same shape as the object pass into `route()`, including nested objects. The leaves of the map are `Route` objects, which you can see if you inspect the type of the `routes` variable in your IDE.
82
+
83
+ ```ts
84
+ type Routes = typeof routes
85
+ // {
86
+ // home: Route<'ANY', '/'>
87
+ // about: Route<'ANY', '/about'>
88
+ // blog: {
89
+ // index: Route<'ANY', '/blog'>
90
+ // show: Route<'ANY', '/blog/:slug'>
91
+ // },
92
+ // }
93
+ ```
94
+
95
+ The `routes.home` route is a `Route<'ANY', '/'>`, which means it serves any request method (`GET`, `POST`, `PUT`, `DELETE`, etc.) when the URL path is `/`. We'll discuss [routing based on request method](#routing-based-on-request-method) in detail later. But first, let's talk about navigation.
96
+
97
+ ### Links and Form Actions
98
+
99
+ In addition to describing the structure of your routes, route maps also make it easy to generate type-safe links and form actions using the `href()` function on a route. The example below is a small site with a home page and a "Contact Us" page.
100
+
101
+ Note: We're using the [`createHtmlResponse` helper from `response`](https://github.com/remix-run/remix/tree/main/packages/response#readme) below to create `Response`s with `Content-Type: text/html`. We're also using the `html` template tag to create safe HTML strings to use in the response body.
102
+
103
+ ```ts
104
+ import { route } from 'remix/routes'
105
+ import { createRouter } from 'remix/router'
106
+ import { html } from 'remix/html-template'
107
+ import { createHtmlResponse } from 'remix/response/html'
108
+
109
+ let routes = route({
110
+ home: '/',
111
+ contact: '/contact',
112
+ })
113
+
114
+ let router = createRouter()
115
+
116
+ // Register an action for `GET /`
117
+ router.get(routes.home, () => {
118
+ return createHtmlResponse(`
119
+ <html>
120
+ <body>
121
+ <h1>Home</h1>
122
+ <p>
123
+ <a href="${routes.contact.href()}">Contact Us</a>
124
+ </p>
125
+ </body>
126
+ </html>
127
+ `)
128
+ })
129
+
130
+ // Register an action for `GET /contact`
131
+ router.get(routes.contact, () => {
132
+ return createHtmlResponse(`
133
+ <html>
134
+ <body>
135
+ <h1>Contact Us</h1>
136
+ <form method="POST" action="${routes.contact.href()}">
137
+ <div>
138
+ <label for="message">Message</label>
139
+ <input type="text" name="message" />
140
+ </div>
141
+ <button type="submit">Send</button>
142
+ </form>
143
+ <footer>
144
+ <p>
145
+ <a href="${routes.home.href()}">Home</a>
146
+ </p>
147
+ </footer>
148
+ </body>
149
+ </html>
150
+ `)
151
+ })
152
+
153
+ // Register an action for `POST /contact`
154
+ router.post(routes.contact, ({ get }) => {
155
+ // POST actions can read parsed FormData from request context using FormData
156
+ // as the context key after the formData middleware has run.
157
+ let formData = get(FormData)
158
+ let message = formData.get('message') as string
159
+ let body = html`
160
+ <html>
161
+ <body>
162
+ <h1>Thanks!</h1>
163
+ <div>
164
+ <p>You said: ${message}</p>
165
+ </div>
166
+ <footer>
167
+ <p>
168
+ <a href="${routes.home.href()}">Home</a>
169
+ </p>
170
+ </footer>
171
+ </body>
172
+ </html>
173
+ `
174
+
175
+ return createHtmlResponse(body)
176
+ })
177
+ ```
178
+
179
+ ### Routing Based on Request Method
180
+
181
+ In the example above, both the `home` and `contact` routes are able to be registered for any incoming [`request.method`](https://developer.mozilla.org/en-US/docs/Web/API/Request/method). If you inspect their types, you'll see:
182
+
183
+ ```tsx
184
+ type HomeRoute = typeof routes.home // Route<'ANY', '/'>
185
+ type ContactRoute = typeof routes.contact // Route<'ANY', '/contact'>
186
+ ```
187
+
188
+ We used `router.get()` and `router.post()` to register actions on each route specifically for the `GET` and `POST` request methods.
189
+
190
+ However, we can also encode the request method into the route definition itself using the `method` property on the route. When you include the `method` in the route definition, `router.map()` will register the action only for that specific request method. This can be more convenient than using `router.get()` and `router.post()` to register actions one at a time.
191
+
192
+ ```ts
193
+ import * as assert from 'node:assert/strict'
194
+ import { createRouter } from 'remix/router'
195
+ import { route } from 'remix/routes'
196
+
197
+ let routes = route({
198
+ home: { method: 'GET', pattern: '/' },
199
+ contact: {
200
+ index: { method: 'GET', pattern: '/contact' },
201
+ action: { method: 'POST', pattern: '/contact' },
202
+ },
203
+ })
204
+
205
+ type Routes = typeof routes
206
+ // Each route is now typed with a specific request method.
207
+ // {
208
+ // home: Route<'GET', '/'>,
209
+ // contact: {
210
+ // index: Route<'GET', '/contact'>,
211
+ // action: Route<'POST', '/contact'>,
212
+ // },
213
+ // }
214
+
215
+ let router = createRouter()
216
+
217
+ router.map(routes, {
218
+ actions: {
219
+ home({ method }) {
220
+ assert.equal(method, 'GET')
221
+ return new Response('Home')
222
+ },
223
+ },
224
+ })
225
+
226
+ router.map(routes.contact, {
227
+ actions: {
228
+ index({ method }) {
229
+ assert.equal(method, 'GET')
230
+ return new Response('Contact')
231
+ },
232
+ action({ method }) {
233
+ assert.equal(method, 'POST')
234
+ return new Response('Contact Action')
235
+ },
236
+ },
237
+ })
238
+ ```
239
+
240
+ ### Declaring Routes
241
+
242
+ In addition to the `{ method, pattern }` syntax shown above, the router provides a few shorthand methods that help eliminate some of the boilerplate when building complex route maps:
243
+
244
+ - [`form`](#declaring-form-routes) - creates a route map with an `index` (`GET`) and `action` (`POST`) route. This is well-suited to showing a standard HTML `<form>` and handling its submit action at the same URL.
245
+ - [`resources` (and `resource`)](#resource-based-routes) - creates a route map with a set of resource-based routes, useful when defining RESTful API routes or [Rails-style resource-based routes](https://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default).
246
+
247
+ #### Declaring Form Routes
248
+
249
+ Continuing with [the example of the contact page](#routing-based-on-request-method), let's use the `form` shorthand to make the route map a little less verbose.
250
+
251
+ A `form()` route map contains two routes: `index` and `action`. The `index` route is a `GET` route that shows the form, and the `action` route is a `POST` route that handles the form submission.
252
+
253
+ ```tsx
254
+ import { createRouter } from 'remix/router'
255
+ import { route, form } from 'remix/routes'
256
+ import { createHtmlResponse } from 'remix/response/html'
257
+ import { html } from 'remix/html-template'
258
+
259
+ let routes = route({
260
+ home: '/',
261
+ contact: form('contact'),
262
+ })
263
+
264
+ type Routes = typeof routes
265
+ // {
266
+ // home: Route<'ANY', '/'>
267
+ // contact: {
268
+ // index: Route<'GET', '/contact'> - Shows the form
269
+ // action: Route<'POST', '/contact'> - Handles the form submission
270
+ // },
271
+ // }
272
+
273
+ let router = createRouter()
274
+
275
+ router.map(routes, {
276
+ actions: {
277
+ home() {
278
+ return createHtmlResponse(`
279
+ <html>
280
+ <body>
281
+ <h1>Home</h1>
282
+ <footer>
283
+ <p>
284
+ <a href="${routes.contact.index.href()}">Contact Us</a>
285
+ </p>
286
+ </footer>
287
+ </body>
288
+ </html>
289
+ `)
290
+ },
291
+ },
292
+ })
293
+
294
+ router.map(routes.contact, {
295
+ actions: {
296
+ // GET /contact - shows the form
297
+ index() {
298
+ return createHtmlResponse(`
299
+ <html>
300
+ <body>
301
+ <h1>Contact Us</h1>
302
+ <form method="POST" action="${routes.contact.action.href()}">
303
+ <label for="message">Message</label>
304
+ <input type="text" name="message" />
305
+ <button type="submit">Send</button>
306
+ </form>
307
+ </body>
308
+ </html>
309
+ `)
310
+ },
311
+ // POST /contact - handles the form submission
312
+ action({ get }) {
313
+ let formData = get(FormData)
314
+ let message = formData.get('message') as string
315
+ let body = html`
316
+ <html>
317
+ <body>
318
+ <h1>Thanks!</h1>
319
+ <p>You said: ${message}</p>
320
+
321
+ <p>
322
+ Got more to say? <a href="${routes.contact.index.href()}">Send another message</a>
323
+ </p>
324
+ </body>
325
+ </html>
326
+ `
327
+
328
+ return createHtmlResponse(body)
329
+ },
330
+ },
331
+ })
332
+ ```
333
+
334
+ #### Resource-based Routes
335
+
336
+ The router provides a `resources()` helper that creates a route map with a set of resource-based routes, useful when defining RESTful API routes or modeling resources in a web application ([similar to Rails' `resources` helper](https://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default)). You can think of "resources" as a way to define routes for a collection of related resources, like products, books, users, etc.
337
+
338
+ ```ts
339
+ import { createRouter } from 'remix/router'
340
+ import { route, resources } from 'remix/routes'
341
+
342
+ let routes = route({
343
+ brands: {
344
+ ...resources('brands', { only: ['index', 'show'] }),
345
+ products: resources('brands/:brandId/products', {
346
+ only: ['index', 'show'],
347
+ }),
348
+ },
349
+ })
350
+
351
+ type Routes = typeof routes
352
+ // {
353
+ // brands: {
354
+ // index: Route<'GET', '/brands'>
355
+ // show: Route<'GET', '/brands/:id'>
356
+ // products: {
357
+ // index: Route<'GET', '/brands/:brandId/products'>
358
+ // show: Route<'GET', '/brands/:brandId/products/:id'>
359
+ // },
360
+ // },
361
+ // }
362
+
363
+ let router = createRouter()
364
+
365
+ router.map(routes.brands, {
366
+ actions: {
367
+ // GET /brands
368
+ index() {
369
+ return new Response('Brands Index')
370
+ },
371
+ // GET /brands/:id
372
+ show({ params }) {
373
+ return new Response(`Brand ${params.id}`)
374
+ },
375
+ },
376
+ })
377
+
378
+ router.map(routes.brands.products, {
379
+ actions: {
380
+ // GET /brands/:brandId/products
381
+ index() {
382
+ return new Response('Products Index')
383
+ },
384
+ // GET /brands/:brandId/products/:id
385
+ show({ params }) {
386
+ return new Response(`Brand ${params.brandId}, Product ${params.id}`)
387
+ },
388
+ },
389
+ })
390
+ ```
391
+
392
+ The `resource()` helper creates a route map for a single resource (i.e. not something that is part of a collection). This is useful when defining operations on a singleton resource, like a user profile.
393
+
394
+ ```tsx
395
+ import { createRouter } from 'remix/router'
396
+ import { route, resources, resource } from 'remix/routes'
397
+
398
+ let routes = route({
399
+ user: {
400
+ ...resources('users', { only: ['index', 'show'] }),
401
+ profile: resource('users/:userId/profile', { only: ['show', 'edit', 'update'] }),
402
+ },
403
+ })
404
+
405
+ type Routes = typeof routes
406
+ // {
407
+ // user: {
408
+ // index: Route<'GET', '/users'>
409
+ // show: Route<'GET', '/users/:id'>
410
+ // profile: {
411
+ // show: Route<'GET', '/users/:userId/profile'>
412
+ // edit: Route<'GET', '/users/:userId/profile/edit'>
413
+ // update: Route<'PUT', '/users/:userId/profile'>
414
+ // },
415
+ // },
416
+ // }
417
+ ```
418
+
419
+ In both of the examples above we used the `only` option to limit the routes generated by `resources()`/`resource()` to only the routes we needed. Without the `only` option, a `resources('users')` route map contains 7 routes: `index`, `new`, `show`, `create`, `edit`, `update`, and `destroy`.
420
+
421
+ ```tsx
422
+ let routes = resources('users')
423
+ type Routes = typeof routes
424
+ // {
425
+ // index: Route<'GET', '/users'> - Lists all users
426
+ // new: Route<'GET', '/users/new'> - Shows a form to create a new user
427
+ // show: Route<'GET', '/users/:id'> - Shows a single user
428
+ // create: Route<'POST', '/users'> - Creates a new user
429
+ // edit: Route<'GET', '/users/:id/edit'> - Shows a form to edit a user
430
+ // update: Route<'PUT', '/users/:id'> - Updates a user
431
+ // destroy: Route<'DELETE', '/users/:id'> - Deletes a user
432
+ // }
433
+ ```
434
+
435
+ Similarly, a `resource('profile')` route map contains 6 routes: `new`, `show`, `create`, `edit`, `update`, and `destroy`. There is no `index` route because a `resource()` represents a singleton resource, not a collection, so there is no collection view.
436
+
437
+ ```tsx
438
+ let routes = resource('profile')
439
+ type Routes = typeof routes
440
+ // {
441
+ // new: Route<'GET', '/profile/new'> - Shows a form to create the profile
442
+ // show: Route<'GET', '/profile'> - Shows the profile
443
+ // create: Route<'POST', '/profile'> - Creates the profile
444
+ // edit: Route<'GET', '/profile/edit'> - Shows a form to edit the profile
445
+ // update: Route<'PUT', '/profile'> - Updates the profile
446
+ // destroy: Route<'DELETE', '/profile'> - Deletes the profile
447
+ // }
448
+ ```
449
+
450
+ Resource route names may be customized using the `names` option when you'd prefer not to use the default `index`/`new`/`show`/`create`/`edit`/`update`/`destroy` route names.
451
+
452
+ ```tsx
453
+ import { createRouter } from 'remix/router'
454
+ import { route, resources } from 'remix/routes'
455
+
456
+ let routes = route({
457
+ users: resources('users', {
458
+ only: ['index', 'show'],
459
+ names: { index: 'list', show: 'view' },
460
+ }),
461
+ })
462
+ type Routes = typeof routes.users
463
+ // {
464
+ // list: Route<'GET', '/users'> - Lists all users
465
+ // view: Route<'GET', '/users/:id'> - Shows a single user
466
+ // }
467
+ ```
468
+
469
+ If you want to use a param name other than `id`, you can use the `param` option.
470
+
471
+ ```tsx
472
+ import { createRouter } from 'remix/router'
473
+ import { route, resources } from 'remix/routes'
474
+
475
+ let routes = route({
476
+ users: resources('users', {
477
+ only: ['index', 'show', 'edit', 'update'],
478
+ param: 'userId',
479
+ }),
480
+ })
481
+ type Routes = typeof routes.users
482
+ // {
483
+ // index: Route<'GET', '/users'> - Lists all users
484
+ // show: Route<'GET', '/users/:userId'> - Shows a single user
485
+ // edit: Route<'GET', '/users/:userId/edit'> - Shows a form to edit a user
486
+ // update: Route<'PUT', '/users/:userId'> - Updates a user
487
+ // }
488
+ ```
489
+
490
+ You can use the `exclude` option to exclude routes from being generated.
491
+
492
+ ```tsx
493
+ let routes = resources('users', { exclude: ['edit', 'update', 'destroy'] })
494
+ type Routes = typeof routes
495
+ // {
496
+ // index: Route<'GET', '/users'> - Lists all users
497
+ // new: Route<'GET', '/users/new'> - Shows a form to create a new user
498
+ // show: Route<'GET', '/users/:userId'> - Shows a single user
499
+ // create: Route<'POST', '/users'> - Creates a new user
500
+ // }
501
+ ```
502
+
503
+ ### Controllers and Middleware
504
+
505
+ Middleware functions run code before and/or after actions. They are a powerful way to add functionality to your app.
506
+
507
+ A basic logging middleware might look like this:
508
+
509
+ ```ts
510
+ import type { Middleware } from 'remix/router'
511
+
512
+ // You can use the `Middleware` type to type middleware functions.
513
+ function logger(): Middleware {
514
+ return async (context, next) => {
515
+ let start = new Date()
516
+
517
+ // Call next() to invoke the next middleware or action in the chain.
518
+ let response = await next()
519
+
520
+ let end = new Date()
521
+ let duration = end.getTime() - start.getTime()
522
+
523
+ console.log(`${context.request.method} ${context.request.url} ${response.status} ${duration}ms`)
524
+
525
+ return response
526
+ }
527
+ }
528
+
529
+ // Use it like this:
530
+ let router = createRouter({
531
+ middleware: [logger()],
532
+ })
533
+ ```
534
+
535
+ Middleware is typically built as a function that returns a middleware function. This allows you to pass options to the middleware function if needed. For example, the `auth()` middleware below allows you to pass a `token` option that is used to authenticate the request.
536
+
537
+ ```tsx
538
+ interface AuthOptions {
539
+ token: string
540
+ }
541
+
542
+ function auth(options?: AuthOptions): Middleware {
543
+ let token = options?.token ?? 'secret'
544
+
545
+ return (context, next) => {
546
+ if (context.headers.get('Authorization') !== `Bearer ${token}`) {
547
+ return new Response('Unauthorized', { status: 401 })
548
+ }
549
+ return next()
550
+ }
551
+ }
552
+ ```
553
+
554
+ Middleware can store values in request context with a key. To make that value available as `context.db`, add `property: 'db'` to the middleware type and pass `{ property: 'db' }` to `context.set()`:
555
+
556
+ ```ts
557
+ import { createContextKey, type Middleware } from 'remix/router'
558
+
559
+ interface Database {
560
+ findMany(): Promise<unknown[]>
561
+ }
562
+
563
+ const Database = createContextKey<Database>()
564
+
565
+ function loadDatabase(): Middleware<{
566
+ key: typeof Database
567
+ value: Database
568
+ property: 'db'
569
+ }> {
570
+ return async (context) => {
571
+ context.set(Database, await connectDatabase(), { property: 'db' })
572
+ }
573
+ }
574
+
575
+ router.get('/books', async (context) => {
576
+ let books = await context.db.findMany()
577
+ return Response.json(books)
578
+ })
579
+ ```
580
+
581
+ Use `context.db` (or `context.get(Database)`). If two values use the same property name, the router throws.
582
+
583
+ Middleware may be used at three levels: globally on the router, on a controller, or inline on an individual action.
584
+
585
+ Global middleware is added to the router when it is created using the `createRouter({ middleware })` option. This middleware runs before any routes are matched and is useful for doing things like logging, serving static files, profiling, and a variety of other things. Global middleware runs on every request, so it's important to keep them lightweight and fast.
586
+
587
+ Controller middleware runs for every direct action in a controller. Action middleware runs only for one action, whether that action is registered in a controller or directly with `router.map()` or one of the method-specific helpers like `router.get()`, `router.post()`, `router.put()`, `router.delete()`, etc. The object form for actions is `{ handler, middleware? }`, so you can omit `middleware` entirely when you do not need it.
588
+
589
+ A controller's `middleware` applies only to the direct route actions in that controller, and its `actions` object may not include nested route-map keys. Map nested route maps explicitly so each controller owns the direct route actions for one route map.
590
+
591
+ ```tsx
592
+ let routes = route({
593
+ home: '/',
594
+ admin: {
595
+ dashboard: '/admin/dashboard',
596
+ settings: '/admin/settings',
597
+ },
598
+ })
599
+
600
+ let router = createRouter({
601
+ // This middleware runs on all requests.
602
+ middleware: [staticFiles('./public')],
603
+ })
604
+
605
+ router.map(routes.home, () => new Response('Home'))
606
+
607
+ router.map(routes.admin, {
608
+ // This middleware applies to all actions in this controller.
609
+ middleware: [auth({ token: 'secret' })],
610
+ actions: {
611
+ dashboard() {
612
+ return new Response('Dashboard')
613
+ },
614
+ settings: {
615
+ // This middleware applies only to this action.
616
+ middleware: [requireAdmin()],
617
+ handler() {
618
+ return new Response('Settings')
619
+ },
620
+ },
621
+ },
622
+ })
623
+ ```
624
+
625
+ ### Request Context
626
+
627
+ Every action and middleware receives a `context` object with useful properties:
628
+
629
+ ```ts
630
+ const UserKey = createContextKey<{ id: string }>()
631
+
632
+ router.get('/posts/:id', (context) => {
633
+ // request: The original Request object
634
+ console.log(context.request.method) // "GET"
635
+ console.log(context.request.headers.get('Accept'))
636
+
637
+ // url: Parsed URL object
638
+ console.log(context.url.pathname) // "/posts/123"
639
+ console.log(context.url.searchParams.get('sort'))
640
+
641
+ // params: Route parameters (fully typed!)
642
+ console.log(context.params.id) // "123"
643
+
644
+ // set/get: type-safe request-scoped context data on the context object
645
+ context.set(UserKey, currentUser)
646
+ let user = context.get(UserKey)
647
+ if (user == null) throw new Error('Expected current user')
648
+ console.log(user.id)
649
+
650
+ return new Response(`Post ${context.params.id}`)
651
+ })
652
+ ```
653
+
654
+ ### Typed Context Contracts
655
+
656
+ Route params are only half of a handler's type contract. In many apps, handlers also depend on values that middleware loads into request context, like sessions, database connections, or authenticated users.
657
+
658
+ `fetch-router` lets you carry that context contract through the router and into stored controllers and actions. A common pattern is to derive one app-local context type from your router middleware, augment `RouterTypes.context` with it, then use `createAction()` and `createController()` to type stored handlers.
659
+
660
+ ```ts
661
+ import { Auth, requireAuth } from 'remix/middleware/auth'
662
+ import {
663
+ createAction,
664
+ createController,
665
+ type AnyParams,
666
+ type ContextWithParams,
667
+ type MiddlewareContext,
668
+ } from 'remix/router'
669
+ import { route } from 'remix/routes'
670
+ import { loadDatabase } from './middleware/database.ts'
671
+ import { loadSession } from './middleware/session.ts'
672
+
673
+ let routes = route({
674
+ account: '/account',
675
+ })
676
+
677
+ type AuthIdentity = { id: string }
678
+ type RootMiddleware = [ReturnType<typeof loadSession>, ReturnType<typeof loadDatabase>]
679
+
680
+ type AppContext<params extends AnyParams = {}> = ContextWithParams<
681
+ MiddlewareContext<RootMiddleware>,
682
+ params
683
+ >
684
+
685
+ declare module 'remix/router' {
686
+ interface RouterTypes {
687
+ context: AppContext
688
+ }
689
+ }
690
+
691
+ let accountMiddleware = [requireAuth<AuthIdentity>()] as const
692
+ type AccountContext = MiddlewareContext<typeof accountMiddleware, AppContext>
693
+
694
+ let accountAction = createAction<typeof routes.account, AccountContext>(routes.account, {
695
+ middleware: accountMiddleware,
696
+ handler(context) {
697
+ let auth = context.get(Auth)
698
+ return Response.json({ id: auth.identity.id })
699
+ },
700
+ })
701
+
702
+ let accountController = createController<typeof routes, AccountContext>(routes, {
703
+ middleware: accountMiddleware,
704
+ actions: {
705
+ account(context) {
706
+ let auth = context.get(Auth)
707
+ return Response.json({ id: auth.identity.id })
708
+ },
709
+ },
710
+ })
711
+ ```
712
+
713
+ In this example, `RootMiddleware` is the middleware tuple that defines the app context contract. It should include middleware instances. When a middleware is created by a factory function like `loadSession()`, use `ReturnType<typeof loadSession>` so the type describes the middleware value that actually runs. `AccountContext` applies local account middleware on top of that base context before the handler runs.
714
+
715
+ For small apps with a stable tuple-typed runtime array, `MiddlewareContext<typeof middleware>` is a fine shortcut. For larger apps, prefer the named `RootMiddleware` and `AppContext` pattern so runtime middleware assembly and context typing can evolve independently.
716
+
717
+ When manually annotating stored handlers, use `Action<typeof route, Context>` for values that may be either a plain handler function or an action object with optional middleware.
718
+
719
+ #### Middleware Provider Guidance
720
+
721
+ `context.get(key)` returns a defined value when the context type includes the key or the key has a default. Constructor keys like `FormData` are useful, but they do not imply presence; use context transforms for required values and handle `undefined` otherwise.
722
+
723
+ If you're authoring a middleware package that stores values in request context, treat that context contract as part of the package API. A good provider should usually export:
724
+
725
+ - the context key consumers read with `context.get(...)`
726
+ - the middleware that populates that key at runtime, with a `Middleware` context transform that describes the value it provides
727
+ - an optional direct context property for values handlers read frequently
728
+
729
+ Apps can derive request context from the middleware tuple with `MiddlewareContext`. If they need to describe a context shape without a middleware tuple, they can use the core `ContextWithEntry` and `ContextWithEntries` helpers directly.
730
+
731
+ ```ts
732
+ import { createContextKey, type Middleware, type MiddlewareContext } from 'remix/router'
733
+
734
+ // The context key that consumers will need to read from `context.get(...)`
735
+ export const CurrentUser = createContextKey<User | null>()
736
+ const currentUserContextProperty = { property: 'currentUser' } as const
737
+
738
+ // The context effect carried by middleware that sets one context value
739
+ export function loadCurrentUser(): Middleware<{
740
+ key: typeof CurrentUser
741
+ value: User | null
742
+ property: 'currentUser'
743
+ }> {
744
+ return async (context, next) => {
745
+ context.set(CurrentUser, await getCurrentUser(context.request), currentUserContextProperty)
746
+ return next()
747
+ }
748
+ }
749
+
750
+ let middleware = [loadCurrentUser()] as const
751
+ type AppContext = MiddlewareContext<typeof middleware>
752
+
753
+ // Use context.currentUser (or context.get(CurrentUser)).
754
+ // context.currentUser
755
+ // context.get(CurrentUser)
756
+ ```
757
+
758
+ ### Additional Topics
759
+
760
+ #### Scaling Your Application
761
+
762
+ - how to spread controllers across multiple files
763
+
764
+ #### Error Handling and Aborted Requests
765
+
766
+ - wrap `router.fetch()` in a try/catch to handle errors
767
+ - `AbortError` is thrown when a request is aborted
768
+
769
+ #### Content Negotiation
770
+
771
+ - use `Accept.from()` from `remix/headers` to serve different responses based on the client's `Accept` header
772
+ - maybe put this on `context.accepts()` for convenience?
773
+
774
+ #### Sessions
775
+
776
+ - use a custom `sessionStorage` implementation to store session data
777
+ - use `session.get()` and `session.set()` to get and set session data
778
+ - use `session.flash()` to set a flash message
779
+ - use `session.destroy()` to destroy the session
780
+
781
+ #### Form Data and File Uploads
782
+
783
+ - use the `formData()` middleware to parse the `FormData` object from the request body
784
+ - use `context.formData` or `context.get(FormData)` to access parsed form data
785
+ - use `context.formData.get(name)`/`getAll(name)` to access uploaded files
786
+ - use the `uploadHandler` option of the `formData()` middleware to handle file uploads
787
+
788
+ #### Request Method Override
789
+
790
+ - use the `methodOverride()` middleware to override the request method
791
+ - use a hidden `<input name="_method" value="...">` to override the request method
792
+
793
+ ### Response Helpers
794
+
795
+ Response helpers for creating common HTTP responses are available in the [`response`](https://github.com/remix-run/remix/tree/main/packages/response) package:
796
+
797
+ ```tsx
798
+ import { createFileResponse } from 'remix/response/file'
799
+ import { createHtmlResponse } from 'remix/response/html'
800
+ import { createRedirectResponse } from 'remix/response/redirect'
801
+ import { compressResponse } from 'remix/response/compress'
802
+
803
+ let response = createHtmlResponse('<h1>Hello</h1>')
804
+ let response = Response.json({ message: 'Hello' })
805
+ let response = createRedirectResponse('/')
806
+ let response = compressResponse(uncompressedResponse, request)
807
+ ```
808
+
809
+ See the [`response` documentation](https://github.com/remix-run/remix/tree/main/packages/response#readme) for more details.
810
+
811
+ ### Working with HTML
812
+
813
+ For working with HTML strings and safe HTML interpolation, see the [`html-template`](https://github.com/remix-run/remix/tree/main/packages/html-template) package. It provides a `html` template tag with automatic escaping to prevent XSS vulnerabilities.
814
+
815
+ ```ts
816
+ import { html } from 'remix/html-template'
817
+ import { createHtmlResponse } from 'remix/response/html'
818
+
819
+ // Use the template tag to escape unsafe variables in HTML.
820
+ let unsafe = '<script>alert(1)</script>'
821
+ let response = createHtmlResponse(html`<h1>${unsafe}</h1>`, { status: 400 })
822
+ ```
823
+
824
+ The `html.raw` template tag can be used to interpolate values without escaping them. This has the same semantics as `String.raw` but for HTML snippets that have already been escaped or are from trusted sources:
825
+
826
+ ```ts
827
+ // Use html.raw as a template tag to skip escaping interpolations
828
+ let safeHtml = '<b>Bold</b>'
829
+ let content = html.raw`<div class="content">${safeHtml}</div>`
830
+ let response = createHtmlResponse(content)
831
+
832
+ // This is particularly useful when building HTML from multiple safe fragments
833
+ let header = '<header>Title</header>'
834
+ let body = '<main>Content</main>'
835
+ let footer = '<footer>Footer</footer>'
836
+ let page = html.raw`
837
+ <!DOCTYPE html>
838
+ <html>
839
+ <body>
840
+ ${header}
841
+ ${body}
842
+ ${footer}
843
+ </body>
844
+ </html>
845
+ `
846
+
847
+ // You can nest html.raw inside html to preserve SafeHtml fragments
848
+ let icon = html.raw`<svg>...</svg>`
849
+ let button = html`<button>${icon} Click me</button>` // icon is not escaped
850
+ ```
851
+
852
+ **Warning**: Only use `html.raw` with trusted content. Unlike the regular `html` template tag, `html.raw` does not escape its interpolations, which can lead to XSS vulnerabilities if used with untrusted user input.
853
+
854
+ See the [`html-template` documentation](https://github.com/remix-run/remix/tree/main/packages/html-template#readme) for more details.
855
+
856
+ ### Testing
857
+
858
+ Testing is straightforward because `fetch-router` uses the standard `fetch()` API:
859
+
860
+ ```ts
861
+ import * as assert from 'node:assert/strict'
862
+ import { describe, it } from 'node:test'
863
+
864
+ describe('blog routes', () => {
865
+ it('creates a new post', async () => {
866
+ let response = await router.fetch('https://api.remix.run/posts', {
867
+ method: 'POST',
868
+ headers: { 'Content-Type': 'application/json' },
869
+ body: JSON.stringify({ title: 'Hello', content: 'World' }),
870
+ })
871
+
872
+ assert.equal(response.status, 201)
873
+ let post = await response.json()
874
+ assert.equal(post.title, 'Hello')
875
+ })
876
+
877
+ it('returns 404 for missing posts', async () => {
878
+ let response = await router.fetch('https://api.remix.run/posts/not-found')
879
+ assert.equal(response.status, 404)
880
+ })
881
+ })
882
+ ```
883
+
884
+ No special test harness or mocking required! Just use `fetch()` like you would in production.
885
+
886
+ ## Related Packages
887
+
888
+ - [auth-middleware](https://github.com/remix-run/remix/tree/main/packages/auth-middleware) - Request authentication and route protection helpers
889
+ - [session-middleware](https://github.com/remix-run/remix/tree/main/packages/session-middleware) - Load and persist sessions in request context
890
+ - [form-data-middleware](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) - Parse request bodies into `context.formData` (or `context.get(FormData)`)
891
+ - [response](https://github.com/remix-run/remix/tree/main/packages/response) - Response helpers for HTML, JSON, files, and redirects
892
+
893
+ ## Related Work
894
+
895
+ - [headers](https://github.com/remix-run/remix/tree/main/packages/headers) - A library for working with HTTP headers
896
+ - [form-data-parser](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - A library for parsing multipart/form-data requests
897
+ - [route-pattern](https://github.com/remix-run/remix/tree/main/packages/route-pattern) - The pattern matching library that powers `fetch-router`
898
+ - [Express](https://expressjs.com/) - The classic Node.js web framework
899
+
900
+ ## License
901
+
902
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)