create-acrylblends 0.0.0-stage → 0.1.0

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 (106) hide show
  1. package/bin/create-acrylblends.js +4 -0
  2. package/dist/index.d.ts +56 -0
  3. package/dist/index.js +274 -0
  4. package/dist/index.js.map +1 -0
  5. package/docs/framework/LICENSE +23 -0
  6. package/docs/framework/README.md +63 -0
  7. package/docs/framework/UPSTREAM.md +3 -0
  8. package/docs/framework/agents/index.md +24 -0
  9. package/docs/framework/api/acryl.md +259 -0
  10. package/docs/framework/api/context.md +527 -0
  11. package/docs/framework/api/exception.md +80 -0
  12. package/docs/framework/api/index.md +12 -0
  13. package/docs/framework/api/presets.md +65 -0
  14. package/docs/framework/api/request.md +396 -0
  15. package/docs/framework/api/routing.md +303 -0
  16. package/docs/framework/api/summary.md +140 -0
  17. package/docs/framework/concepts/benchmarks.md +26 -0
  18. package/docs/framework/concepts/developer-experience.md +5 -0
  19. package/docs/framework/concepts/middleware.md +21 -0
  20. package/docs/framework/concepts/motivation.md +14 -0
  21. package/docs/framework/concepts/routers.md +63 -0
  22. package/docs/framework/concepts/stacks.md +235 -0
  23. package/docs/framework/concepts/web-standard.md +37 -0
  24. package/docs/framework/cordis/acryl-in-a-cordis-app.md +146 -0
  25. package/docs/framework/cordis/concepts.md +105 -0
  26. package/docs/framework/cordis/middleware.md +8 -0
  27. package/docs/framework/cordis/migration-from-0-3.md +111 -0
  28. package/docs/framework/cordis/migration-from-0-4.md +14 -0
  29. package/docs/framework/cordis/migration-from-dsh-webserver.md +103 -0
  30. package/docs/framework/cordis/mounting.md +85 -0
  31. package/docs/framework/cordis/node-server.md +75 -0
  32. package/docs/framework/cordis/plugin-lifecycle.md +111 -0
  33. package/docs/framework/cordis/surfaces.md +71 -0
  34. package/docs/framework/cordis/transport-and-cancellation.md +57 -0
  35. package/docs/framework/cordis/validation.md +135 -0
  36. package/docs/framework/getting-started/basic.md +196 -0
  37. package/docs/framework/getting-started/bun.md +221 -0
  38. package/docs/framework/getting-started/nodejs.md +344 -0
  39. package/docs/framework/getting-started/other-runtimes.md +23 -0
  40. package/docs/framework/guides/best-practices.md +231 -0
  41. package/docs/framework/guides/create-acrylblends.md +79 -0
  42. package/docs/framework/guides/examples.md +3 -0
  43. package/docs/framework/guides/faq.md +19 -0
  44. package/docs/framework/guides/helpers.md +33 -0
  45. package/docs/framework/guides/jsx-dom.md +299 -0
  46. package/docs/framework/guides/jsx.md +440 -0
  47. package/docs/framework/guides/middleware.md +253 -0
  48. package/docs/framework/guides/rpc.md +886 -0
  49. package/docs/framework/guides/testing.md +133 -0
  50. package/docs/framework/guides/validation.md +383 -0
  51. package/docs/framework/helpers/accepts.md +60 -0
  52. package/docs/framework/helpers/adapter.md +87 -0
  53. package/docs/framework/helpers/conninfo.md +112 -0
  54. package/docs/framework/helpers/cookie.md +232 -0
  55. package/docs/framework/helpers/css.md +285 -0
  56. package/docs/framework/helpers/dev.md +67 -0
  57. package/docs/framework/helpers/factory.md +180 -0
  58. package/docs/framework/helpers/html.md +123 -0
  59. package/docs/framework/helpers/jwt.md +184 -0
  60. package/docs/framework/helpers/proxy.md +118 -0
  61. package/docs/framework/helpers/route.md +143 -0
  62. package/docs/framework/helpers/ssg.md +393 -0
  63. package/docs/framework/helpers/streaming.md +119 -0
  64. package/docs/framework/helpers/testing.md +101 -0
  65. package/docs/framework/helpers/websocket.md +215 -0
  66. package/docs/framework/middleware/builtin/basic-auth.md +165 -0
  67. package/docs/framework/middleware/builtin/bearer-auth.md +138 -0
  68. package/docs/framework/middleware/builtin/body-limit.md +71 -0
  69. package/docs/framework/middleware/builtin/cache.md +122 -0
  70. package/docs/framework/middleware/builtin/combine.md +94 -0
  71. package/docs/framework/middleware/builtin/compress.md +59 -0
  72. package/docs/framework/middleware/builtin/context-storage.md +78 -0
  73. package/docs/framework/middleware/builtin/cors.md +135 -0
  74. package/docs/framework/middleware/builtin/csrf.md +105 -0
  75. package/docs/framework/middleware/builtin/etag.md +54 -0
  76. package/docs/framework/middleware/builtin/ip-restriction.md +69 -0
  77. package/docs/framework/middleware/builtin/jsx-renderer.md +286 -0
  78. package/docs/framework/middleware/builtin/jwk.md +192 -0
  79. package/docs/framework/middleware/builtin/jwt.md +145 -0
  80. package/docs/framework/middleware/builtin/language.md +232 -0
  81. package/docs/framework/middleware/builtin/logger.md +74 -0
  82. package/docs/framework/middleware/builtin/loopback-only.md +31 -0
  83. package/docs/framework/middleware/builtin/method-not-allowed.md +48 -0
  84. package/docs/framework/middleware/builtin/method-override.md +80 -0
  85. package/docs/framework/middleware/builtin/mount.md +106 -0
  86. package/docs/framework/middleware/builtin/pretty-json.md +53 -0
  87. package/docs/framework/middleware/builtin/request-id.md +94 -0
  88. package/docs/framework/middleware/builtin/secure-headers.md +247 -0
  89. package/docs/framework/middleware/builtin/timeout.md +98 -0
  90. package/docs/framework/middleware/builtin/timing.md +101 -0
  91. package/docs/framework/middleware/builtin/trailing-slash.md +74 -0
  92. package/docs/framework/middleware/third-party.md +4 -0
  93. package/docs/framework/upstream.json +9 -0
  94. package/package.json +47 -3
  95. package/templates/bun/gitignore +1 -0
  96. package/templates/bun/package.json +19 -0
  97. package/templates/bun/src/index.ts +7 -0
  98. package/templates/bun/test/app.test.ts +8 -0
  99. package/templates/bun/tsconfig.json +15 -0
  100. package/templates/nodejs/gitignore +1 -0
  101. package/templates/nodejs/package.json +22 -0
  102. package/templates/nodejs/src/app.ts +7 -0
  103. package/templates/nodejs/src/index.ts +9 -0
  104. package/templates/nodejs/test/app.test.ts +9 -0
  105. package/templates/nodejs/tsconfig.json +16 -0
  106. package/README.md +0 -3
@@ -0,0 +1,886 @@
1
+ # RPC
2
+
3
+ The RPC feature allows sharing of the API specifications between the server and the client.
4
+
5
+ First, export the `typeof` your Acryl app (commonly called `AppType`) - or just the routes you want available to the client - from your server code.
6
+
7
+ By accepting `AppType` as a generic parameter, the Acryl Client can infer both the input type(s) specified by the Validator, and the output type(s) emitted by handlers returning `c.json()`.
8
+
9
+ > [!NOTE]
10
+ > For the RPC types to work properly in a monorepo, in both the Client's and Server's tsconfig.json files, set `"strict": true` in `compilerOptions`. Read more.
11
+
12
+ ## Server
13
+
14
+ All you need to do on the server side is to write a validator and create a variable `route`. The following example uses Zod Validator.
15
+
16
+ ```ts
17
+ const route = app.post(
18
+ '/posts',
19
+ schemaValidator(
20
+ 'form',
21
+ z.object({
22
+ title: z.string(),
23
+ body: z.string(),
24
+ })
25
+ ),
26
+ (c) => {
27
+ // ...
28
+ return c.json(
29
+ {
30
+ ok: true,
31
+ message: 'Created!',
32
+ },
33
+ 201
34
+ )
35
+ }
36
+ )
37
+ ```
38
+
39
+ > [!TIP]
40
+ > The Standard Schema Validator works as well, so you can use any Standard Schema library such as Valibot.
41
+
42
+ Then, export the type to share the API spec with the Client.
43
+
44
+ ```ts
45
+ export type AppType = typeof route
46
+ ```
47
+
48
+ ## Client
49
+
50
+ On the Client side, import `hc` and `AppType` first.
51
+
52
+ ```ts
53
+ import type { AppType } from '.'
54
+ import { hc } from 'acrylblends/client'
55
+ ```
56
+
57
+ `hc` is a function to create a client. Pass `AppType` as Generics and specify the server URL as an argument.
58
+
59
+ ```ts
60
+ const client = hc<AppType>('http://localhost:8787/')
61
+ ```
62
+
63
+ Call `client.{path}.{method}` and pass the data you wish to send to the server as an argument.
64
+
65
+ ```ts
66
+ const res = await client.posts.$post({
67
+ form: {
68
+ title: 'Hello',
69
+ body: 'Acryl is a cool project',
70
+ },
71
+ })
72
+ ```
73
+
74
+ The `res` is compatible with the "fetch" Response. You can retrieve data from the server with `res.json()`.
75
+
76
+ ```ts
77
+ if (res.ok) {
78
+ const data = await res.json()
79
+ console.log(data.message)
80
+ }
81
+ ```
82
+
83
+ ### Cookies
84
+
85
+ To make the client send cookies with every request, add `{ 'init': { 'credentials": 'include' } }` to the options when you're creating the client.
86
+
87
+ ```ts
88
+ // client.ts
89
+ const client = hc<AppType>('http://localhost:8787/', {
90
+ init: {
91
+ credentials: 'include',
92
+ },
93
+ })
94
+
95
+ // This request will now include any cookies you might have set
96
+ const res = await client.posts.$get({
97
+ query: {
98
+ id: '123',
99
+ },
100
+ })
101
+ ```
102
+
103
+ ## Status code
104
+
105
+ If you explicitly specify the status code, such as `200` or `404`, in `c.json()`, it will be added as a type for passing to the client.
106
+
107
+ ```ts
108
+ // server.ts
109
+ const app = new Acryl().get(
110
+ '/posts',
111
+ schemaValidator(
112
+ 'query',
113
+ z.object({
114
+ id: z.string(),
115
+ })
116
+ ),
117
+ async (c) => {
118
+ const { id } = c.req.valid('query')
119
+ const post: Post | undefined = await getPost(id)
120
+
121
+ if (post === undefined) {
122
+ return c.json({ error: 'not found' }, 404) // Specify 404
123
+ }
124
+
125
+ return c.json({ post }, 200) // Specify 200
126
+ }
127
+ )
128
+
129
+ export type AppType = typeof app
130
+ ```
131
+
132
+ You can get the data by the status code.
133
+
134
+ ```ts
135
+ // client.ts
136
+ const client = hc<AppType>('http://localhost:8787/')
137
+
138
+ const res = await client.posts.$get({
139
+ query: {
140
+ id: '123',
141
+ },
142
+ })
143
+
144
+ if (res.status === 404) {
145
+ const data: { error: string } = await res.json()
146
+ console.log(data.error)
147
+ }
148
+
149
+ if (res.ok) {
150
+ const data: { post: Post } = await res.json()
151
+ console.log(data.post)
152
+ }
153
+
154
+ // { post: Post } | { error: string }
155
+ type ResponseType = InferResponseType<typeof client.posts.$get>
156
+
157
+ // { post: Post }
158
+ type ResponseType200 = InferResponseType<
159
+ typeof client.posts.$get,
160
+ 200
161
+ >
162
+ ```
163
+
164
+ ## Global Response
165
+
166
+ Acryl RPC client doesn't automatically infer response types from global error handlers like `app.onError()` or global middleware. You can use the `ApplyGlobalResponse` type helper to merge global error response types into all routes.
167
+
168
+ ```ts
169
+ import type { ApplyGlobalResponse } from 'acrylblends/client'
170
+
171
+ const app = new Acryl()
172
+ .get('/api/users', (c) => c.json({ users: ['alice', 'bob'] }, 200))
173
+ .onError((err, c) => c.json({ error: err.message }, 500))
174
+
175
+ type AppWithErrors = ApplyGlobalResponse<
176
+ typeof app,
177
+ {
178
+ 500: { json: { error: string } }
179
+ }
180
+ >
181
+
182
+ const client = hc<AppWithErrors>('http://localhost')
183
+ ```
184
+
185
+ Now the client knows about both success and error responses:
186
+
187
+ ```ts
188
+ const res = await client.api.users.$get()
189
+
190
+ if (res.ok) {
191
+ const data = await res.json() // { users: string[] }
192
+ }
193
+
194
+ // InferResponseType includes the global error type
195
+ type ResType = InferResponseType<typeof client.api.users.$get>
196
+ // { users: string[] } | { error: string }
197
+ ```
198
+
199
+ You can also define multiple global error status codes at once:
200
+
201
+ ```ts
202
+ type AppWithErrors = ApplyGlobalResponse<
203
+ typeof app,
204
+ {
205
+ 401: { json: { error: string; message: string } }
206
+ 500: { json: { error: string; message: string } }
207
+ }
208
+ >
209
+ ```
210
+
211
+ ## Not Found
212
+
213
+ If you want to use a client, you should not use `c.notFound()` for the Not Found response. The data that the client gets from the server cannot be inferred correctly.
214
+
215
+ ```ts
216
+ // server.ts
217
+ export const routes = new Acryl().get(
218
+ '/posts',
219
+ schemaValidator(
220
+ 'query',
221
+ z.object({
222
+ id: z.string(),
223
+ })
224
+ ),
225
+ async (c) => {
226
+ const { id } = c.req.valid('query')
227
+ const post: Post | undefined = await getPost(id)
228
+
229
+ if (post === undefined) {
230
+ return c.notFound() // ❌️
231
+ }
232
+
233
+ return c.json({ post })
234
+ }
235
+ )
236
+
237
+ // client.ts
238
+ import { hc } from 'acrylblends/client'
239
+
240
+ const client = hc<typeof routes>('/')
241
+
242
+ const res = await client.posts[':id'].$get({
243
+ param: {
244
+ id: '123',
245
+ },
246
+ })
247
+
248
+ const data = await res.json() // 🙁 data is unknown
249
+ ```
250
+
251
+ Please use `c.json()` and specify the status code for the Not Found Response.
252
+
253
+ ```ts
254
+ export const routes = new Acryl().get(
255
+ '/posts',
256
+ schemaValidator(
257
+ 'query',
258
+ z.object({
259
+ id: z.string(),
260
+ })
261
+ ),
262
+ async (c) => {
263
+ const { id } = c.req.valid('query')
264
+ const post = await getPost(id)
265
+
266
+ if (!post) {
267
+ return c.json({ error: 'not found' }, 404) // Specify 404
268
+ }
269
+
270
+ return c.json({ post }, 200) // Specify 200
271
+ }
272
+ )
273
+ ```
274
+
275
+ Alternatively, you can use module augmentation to extend `NotFoundResponse` interface. This allows `c.notFound()` to return a typed response:
276
+
277
+ ```ts
278
+ // server.ts
279
+ import { Acryl, TypedResponse } from 'acrylblends'
280
+
281
+ declare module 'acrylblends' {
282
+ interface NotFoundResponse
283
+ extends Response,
284
+ TypedResponse<{ error: string }, 404, 'json'> {}
285
+ }
286
+
287
+ const app = new Acryl()
288
+ .get('/posts/:id', async (c) => {
289
+ const post = await getPost(c.req.param('id'))
290
+ if (!post) {
291
+ return c.notFound()
292
+ }
293
+ return c.json({ post }, 200)
294
+ })
295
+ .notFound((c) => c.json({ error: 'not found' }, 404))
296
+
297
+ export type AppType = typeof app
298
+ ```
299
+
300
+ Now the client can correctly infer the 404 response type.
301
+
302
+ ## Path parameters
303
+
304
+ You can also handle routes that include path parameters or query values.
305
+
306
+ ```ts
307
+ const route = app.get(
308
+ '/posts/:id',
309
+ schemaValidator(
310
+ 'query',
311
+ z.object({
312
+ page: z.coerce.number().optional(), // coerce to convert to number
313
+ })
314
+ ),
315
+ (c) => {
316
+ // ...
317
+ return c.json({
318
+ title: 'Night',
319
+ body: 'Time to sleep',
320
+ })
321
+ }
322
+ )
323
+ ```
324
+
325
+ Both path parameters and query values **must** be passed as `string`, even if the underlying value is of a different type.
326
+
327
+ Specify the string you want to include in the path with `param`, and any query values with `query`.
328
+
329
+ ```ts
330
+ const res = await client.posts[':id'].$get({
331
+ param: {
332
+ id: '123',
333
+ },
334
+ query: {
335
+ page: '1', // `string`, converted by the validator to `number`
336
+ },
337
+ })
338
+ ```
339
+
340
+ ### Multiple parameters
341
+
342
+ Handle routes with multiple parameters.
343
+
344
+ ```ts
345
+ const route = app.get(
346
+ '/posts/:postId/:authorId',
347
+ schemaValidator(
348
+ 'query',
349
+ z.object({
350
+ page: z.string().optional(),
351
+ })
352
+ ),
353
+ (c) => {
354
+ // ...
355
+ return c.json({
356
+ title: 'Night',
357
+ body: 'Time to sleep',
358
+ })
359
+ }
360
+ )
361
+ ```
362
+
363
+ Add multiple `['']` to specify params in path.
364
+
365
+ ```ts
366
+ const res = await client.posts[':postId'][':authorId'].$get({
367
+ param: {
368
+ postId: '123',
369
+ authorId: '456',
370
+ },
371
+ query: {},
372
+ })
373
+ ```
374
+
375
+ ### Include slashes
376
+
377
+ `hc` function does not URL-encode the values of `param`. To include slashes in parameters, use [regular expressions](../api/routing.md#regexp).
378
+
379
+ ```ts
380
+ // client.ts
381
+
382
+ // Requests /posts/123/456
383
+ const res = await client.posts[':id'].$get({
384
+ param: {
385
+ id: '123/456',
386
+ },
387
+ })
388
+
389
+ // server.ts
390
+ const route = app.get(
391
+ '/posts/:id{.+}',
392
+ schemaValidator(
393
+ 'param',
394
+ z.object({
395
+ id: z.string(),
396
+ })
397
+ ),
398
+ (c) => {
399
+ // id: 123/456
400
+ const { id } = c.req.valid('param')
401
+ // ...
402
+ }
403
+ )
404
+ ```
405
+
406
+ > [!NOTE]
407
+ > Basic path parameters without regular expressions do not match slashes. If you pass a `param` containing slashes using the hc function, the server might not route as intended. Encoding the parameters using `encodeURIComponent` is the recommended approach to ensure correct routing.
408
+
409
+ ## Headers
410
+
411
+ You can append the headers to the request.
412
+
413
+ ```ts
414
+ const res = await client.search.$get(
415
+ {
416
+ //...
417
+ },
418
+ {
419
+ headers: {
420
+ 'X-Custom-Header': 'Here is Acryl Client',
421
+ 'X-User-Agent': 'hc',
422
+ },
423
+ }
424
+ )
425
+ ```
426
+
427
+ To add a common header to all requests, specify it as an argument to the `hc` function.
428
+
429
+ ```ts
430
+ const client = hc<AppType>('/api', {
431
+ headers: {
432
+ Authorization: 'Bearer TOKEN',
433
+ },
434
+ })
435
+ ```
436
+
437
+ ## `init` option
438
+
439
+ You can pass the fetch's `RequestInit` object to the request as the `init` option. Below is an example of aborting a Request.
440
+
441
+ ```ts no-check
442
+ import { hc } from 'acrylblends/client'
443
+
444
+ const client = hc<AppType>('http://localhost:8787/')
445
+
446
+ const abortController = new AbortController()
447
+ const res = await client.api.posts.$post(
448
+ {
449
+ json: {
450
+ // Request body
451
+ },
452
+ },
453
+ {
454
+ // RequestInit object
455
+ init: {
456
+ signal: abortController.signal,
457
+ },
458
+ }
459
+ )
460
+
461
+ // ...
462
+
463
+ abortController.abort()
464
+ ```
465
+
466
+ > **Note.** A `RequestInit` object defined by `init` takes the highest priority. It could be used to overwrite things set by other options like `body | method | headers`.
467
+
468
+ ## `$url()`
469
+
470
+ You can get a `URL` object for accessing the endpoint by using `$url()`.
471
+
472
+ **Warning.**
473
+
474
+ You have to pass in an absolute URL for this to work. Passing in a relative URL `/` will result in the following error.
475
+
476
+ `Uncaught TypeError: Failed to construct 'URL': Invalid URL`
477
+
478
+ ```ts
479
+ // ❌ Will throw error
480
+ const client = hc<AppType>('/')
481
+ client.api.post.$url()
482
+
483
+ // ✅ Will work as expected
484
+ const client = hc<AppType>('http://localhost:8787/')
485
+ client.api.post.$url()
486
+ ```
487
+
488
+ ```ts
489
+ const route = app
490
+ .get('/api/posts', (c) => c.json({ posts }))
491
+ .get('/api/posts/:id', (c) => c.json({ post }))
492
+
493
+ const client = hc<typeof route>('http://localhost:8787/')
494
+
495
+ let url = client.api.posts.$url()
496
+ console.log(url.pathname) // `/api/posts`
497
+
498
+ url = client.api.posts[':id'].$url({
499
+ param: {
500
+ id: '123',
501
+ },
502
+ })
503
+ console.log(url.pathname) // `/api/posts/123`
504
+ ```
505
+
506
+ ### Typed URL
507
+
508
+ You can pass the base URL as the second type parameter to `hc` to get more precise URL types:
509
+
510
+ ```ts
511
+ const client = hc<typeof route, 'http://localhost:8787'>(
512
+ 'http://localhost:8787/'
513
+ )
514
+
515
+ const url = client.api.posts.$url()
516
+ // url is TypedURL with precise type information
517
+ // including protocol, host, and path
518
+ ```
519
+
520
+ This is useful when you want to use the URL as a type-safe key for libraries like SWR.
521
+
522
+ ## `$path()`
523
+
524
+ `$path()` is similar to `$url()`, but returns a path string instead of a `URL` object. Unlike `$url()`, it does not include the base URL origin, so it works regardless of the base URL you pass to `hc`.
525
+
526
+ ```ts
527
+ const route = app
528
+ .get('/api/posts', (c) => c.json({ posts }))
529
+ .get('/api/posts/:id', (c) => c.json({ post }))
530
+
531
+ const client = hc<typeof route>('http://localhost:8787/')
532
+
533
+ let path = client.api.posts.$path()
534
+ console.log(path) // `/api/posts`
535
+
536
+ path = client.api.posts[':id'].$path({
537
+ param: {
538
+ id: '123',
539
+ },
540
+ })
541
+ console.log(path) // `/api/posts/123`
542
+ ```
543
+
544
+ You can also pass query parameters:
545
+
546
+ ```ts
547
+ const path = client.api.posts.$path({
548
+ query: {
549
+ page: '1',
550
+ limit: '10',
551
+ },
552
+ })
553
+ console.log(path) // `/api/posts?page=1&limit=10`
554
+ ```
555
+
556
+ ## File Uploads
557
+
558
+ You can upload files using a form body:
559
+
560
+ ```ts
561
+ // client
562
+ const res = await client.user.picture.$put({
563
+ form: {
564
+ file: new File([fileToUpload], filename, {
565
+ type: fileToUpload.type,
566
+ }),
567
+ },
568
+ })
569
+ ```
570
+
571
+ ```ts
572
+ // server
573
+ const route = app.put(
574
+ '/user/picture',
575
+ schemaValidator(
576
+ 'form',
577
+ z.object({
578
+ file: z.instanceof(File),
579
+ })
580
+ )
581
+ // ...
582
+ )
583
+ ```
584
+
585
+ ## Custom `fetch` method
586
+
587
+ You can set the custom `fetch` method.
588
+
589
+ In the following example script for Cloudflare Worker, the Service Bindings' `fetch` method is used instead of the default `fetch`.
590
+
591
+ ```toml
592
+ # wrangler.toml
593
+ services = [
594
+ { binding = "AUTH", service = "auth-service" },
595
+ ]
596
+ ```
597
+
598
+ ```ts
599
+ // src/client.ts
600
+ const client = hc<CreateProfileType>('http://localhost', {
601
+ fetch: c.env.AUTH.fetch.bind(c.env.AUTH),
602
+ })
603
+ ```
604
+
605
+ ## Custom query serializer
606
+
607
+ You can customize how query parameters are serialized using the `buildSearchParams` option. This is useful when you need bracket notation for arrays or other custom formats:
608
+
609
+ ```ts
610
+ const client = hc<AppType>('http://localhost', {
611
+ buildSearchParams: (query) => {
612
+ const searchParams = new URLSearchParams()
613
+ for (const [k, v] of Object.entries(query)) {
614
+ if (v === undefined) {
615
+ continue
616
+ }
617
+ if (Array.isArray(v)) {
618
+ v.forEach((item) => searchParams.append(`${k}[]`, item))
619
+ } else {
620
+ searchParams.set(k, v)
621
+ }
622
+ }
623
+ return searchParams
624
+ },
625
+ })
626
+ ```
627
+
628
+ ## Infer
629
+
630
+ Use `InferRequestType` and `InferResponseType` to know the type of object to be requested and the type of object to be returned.
631
+
632
+ ```ts
633
+ import type { InferRequestType, InferResponseType } from 'acrylblends/client'
634
+
635
+ // InferRequestType
636
+ const $post = client.todo.$post
637
+ type ReqType = InferRequestType<typeof $post>['form']
638
+
639
+ // InferResponseType
640
+ type ResType = InferResponseType<typeof $post>
641
+ ```
642
+
643
+ ## Parsing a Response with type-safety helper
644
+
645
+ You can use `parseResponse()` helper to easily parse a Response from `hc` with type-safety.
646
+
647
+ ```ts
648
+ import { parseResponse, DetailedError } from 'acrylblends/client'
649
+
650
+ // result contains the parsed response body (automatically parsed based on Content-Type)
651
+ const result = await parseResponse(client.hello.$get()).catch(
652
+ (e: DetailedError) => {
653
+ console.error(e)
654
+ }
655
+ )
656
+ // parseResponse automatically throws an error if response is not ok
657
+ ```
658
+
659
+ ## Using SWR
660
+
661
+ You can also use a React Hook library such as [SWR](https://swr.vercel.app).
662
+
663
+ ```tsx
664
+ import useSWR from 'swr'
665
+ import { hc } from 'acrylblends/client'
666
+ import type { InferRequestType } from 'acrylblends/client'
667
+ import type { AppType } from '../functions/api/[[route]]'
668
+
669
+ const App = () => {
670
+ const client = hc<AppType>('/api')
671
+ const $get = client.hello.$get
672
+
673
+ const fetcher =
674
+ (arg: InferRequestType<typeof $get>) => async () => {
675
+ const res = await $get(arg)
676
+ return await res.json()
677
+ }
678
+
679
+ const { data, error, isLoading } = useSWR(
680
+ 'api-hello',
681
+ fetcher({
682
+ query: {
683
+ name: 'SWR',
684
+ },
685
+ })
686
+ )
687
+
688
+ if (error) return <div>failed to load</div>
689
+ if (isLoading) return <div>loading...</div>
690
+
691
+ return <h1>{data?.message}</h1>
692
+ }
693
+
694
+ export default App
695
+ ```
696
+
697
+ ## Using RPC with larger applications
698
+
699
+ In the case of a larger application, such as the example mentioned in [Building a larger application](best-practices.md#building-a-larger-application), you need to be careful about the type of inference.
700
+ A simple way to do this is to chain the handlers so that the types are always inferred.
701
+
702
+ ```ts
703
+ // authors.ts
704
+ import { Acryl } from 'acrylblends'
705
+
706
+ const app = new Acryl()
707
+ .get('/', (c) => c.json('list authors'))
708
+ .post('/', (c) => c.json('create an author', 201))
709
+ .get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
710
+
711
+ export default app
712
+ ```
713
+
714
+ ```ts
715
+ // books.ts
716
+ import { Acryl } from 'acrylblends'
717
+
718
+ const app = new Acryl()
719
+ .get('/', (c) => c.json('list books'))
720
+ .post('/', (c) => c.json('create a book', 201))
721
+ .get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
722
+
723
+ export default app
724
+ ```
725
+
726
+ You can then import the sub-routers as you usually would, and make sure you chain their handlers as well, since this is the top level of the app in this case, this is the type we'll want to export.
727
+
728
+ ```ts
729
+ // index.ts
730
+ import { Acryl } from 'acrylblends'
731
+ import authors from './authors'
732
+ import books from './books'
733
+
734
+ const app = new Acryl()
735
+
736
+ const routes = app.route('/authors', authors).route('/books', books)
737
+
738
+ export default app
739
+ export type AppType = typeof routes
740
+ ```
741
+
742
+ You can now create a new client using the registered AppType and use it as you would normally.
743
+
744
+ ## Known issues
745
+
746
+ ### IDE performance
747
+
748
+ When using RPC, the more routes you have, the slower your IDE will become. One of the main reasons for this is that massive amounts of type instantiations are executed to infer the type of your app.
749
+
750
+ For example, suppose your app has a route like this:
751
+
752
+ ```ts
753
+ // app.ts
754
+ export const app = new Acryl().get('foo/:id', (c) =>
755
+ c.json({ ok: true }, 200)
756
+ )
757
+ ```
758
+
759
+ Acryl will infer the type as follows:
760
+
761
+ ```ts
762
+ export const app = Acryl<BlankEnv, BlankSchema, '/'>().get<
763
+ 'foo/:id',
764
+ 'foo/:id',
765
+ JSONRespondReturn<{ ok: boolean }, 200>,
766
+ BlankInput,
767
+ BlankEnv
768
+ >('foo/:id', (c) => c.json({ ok: true }, 200))
769
+ ```
770
+
771
+ This is a type instantiation for a single route. While the user doesn't need to write these type arguments manually, which is a good thing, it's known that type instantiation takes much time. `tsserver` used in your IDE does this time consuming task every time you use the app. If you have a lot of routes, this can slow down your IDE significantly.
772
+
773
+ However, we have some tips to mitigate this issue.
774
+
775
+ #### Acryl version mismatch
776
+
777
+ If your backend is separated from the frontend and lives in a different directory, you need to ensure that the Acryl versions match. If you use one Acryl version on the backend and another on the frontend, you'll run into issues such as "_Type instantiation is excessively deep and possibly infinite_".
778
+
779
+ #### TypeScript project references
780
+
781
+ Like in the case of [Acryl version mismatch](#acrylblends-version-mismatch), you'll run into issues if your backend and frontend are separate. If you want to access code from the backend (`AppType`, for example) on the frontend, you need to use [project references](https://www.typescriptlang.org/docs/handbook/project-references.html). TypeScript's project references allow one TypeScript codebase to access and use code from another TypeScript codebase. _(source: [Acryl RPC And TypeScript Project References](https://catalins.tech/acrylblends-rpc-in-monorepos/))_.
782
+
783
+ #### Compile your code before using it (recommended)
784
+
785
+ `tsc` can do heavy tasks like type instantiation at compile time! Then, `tsserver` doesn't need to instantiate all the type arguments every time you use it. It will make your IDE a lot faster!
786
+
787
+ Compiling your client including the server app gives you the best performance. Put the following code in your project:
788
+
789
+ ```ts
790
+ import { app } from './app'
791
+ import { hc } from 'acrylblends/client'
792
+
793
+ // this is a trick to calculate the type when compiling
794
+ const client = hc<typeof app>('')
795
+ export type Client = typeof client
796
+
797
+ export const hcWithType = (...args: Parameters<typeof hc>): Client =>
798
+ hc<typeof app>(...args)
799
+ ```
800
+
801
+ After compiling, you can use `hcWithType` instead of `hc` to get the client with the type already calculated.
802
+
803
+ ```ts
804
+ const client = hcWithType('http://localhost:8787/')
805
+ const res = await client.posts.$post({
806
+ form: {
807
+ title: 'Hello',
808
+ body: 'Acryl is a cool project',
809
+ },
810
+ })
811
+ ```
812
+
813
+ If your project is a monorepo, this solution does fit well. Using a tool like [`turborepo`](https://turbo.build/repo/docs), you can easily separate the server project and the client project and get better integration managing dependencies between them. Here is a working example.
814
+
815
+ You can also coordinate your build process manually with tools like `concurrently` or `npm-run-all`.
816
+
817
+ #### Specify type arguments manually
818
+
819
+ This is a bit cumbersome, but you can specify type arguments manually to avoid type instantiation.
820
+
821
+ ```ts
822
+ const app = new Acryl().get<'foo/:id'>('foo/:id', (c) =>
823
+ c.json({ ok: true }, 200)
824
+ )
825
+ ```
826
+
827
+ Specifying just a single type argument makes a difference in performance, while it may take you a lot of time and effort if you have a lot of routes.
828
+
829
+ #### Split your app and client into multiple files
830
+
831
+ As described in [Using RPC with larger applications](#using-rpc-with-larger-applications), you can split your app into multiple apps. You can also create a client for each app:
832
+
833
+ ```ts
834
+ // authors-cli.ts
835
+ import { app as authorsApp } from './authors'
836
+ import { hc } from 'acrylblends/client'
837
+
838
+ const authorsClient = hc<typeof authorsApp>('/authors')
839
+
840
+ // books-cli.ts
841
+ import { app as booksApp } from './books'
842
+ import { hc } from 'acrylblends/client'
843
+
844
+ const booksClient = hc<typeof booksApp>('/books')
845
+ ```
846
+
847
+ This way, `tsserver` doesn't need to instantiate types for all routes at once.
848
+
849
+ ### Handlers that return a promise chain
850
+
851
+ A handler that returns a `.then()` chain directly loses its response type, so the client infers `unknown`:
852
+
853
+ ```ts
854
+ const app = new Acryl().get('/', (c) =>
855
+ Promise.resolve({ hello: 'world' }).then((d) => c.json(d))
856
+ )
857
+
858
+ const client = hc<typeof app>('')
859
+ const res = await client.index.$get()
860
+ const data = await res.json() // unknown
861
+ ```
862
+
863
+ This is a TypeScript inference limitation - the response type cannot be inferred through a `.then()` chain. Use `async`/`await` instead:
864
+
865
+ ```ts
866
+ const app = new Acryl().get('/', async (c) => {
867
+ const d = await Promise.resolve({ hello: 'world' })
868
+ return c.json(d)
869
+ })
870
+
871
+ const client = hc<typeof app>('')
872
+ const res = await client.index.$get()
873
+ const data = await res.json() // { hello: string }
874
+ ```
875
+
876
+ If you cannot avoid the chain, annotating `then()` also works:
877
+
878
+ ```ts
879
+ import type { TypedResponse } from 'acrylblends/types'
880
+
881
+ const app = new Acryl().get('/', (c) =>
882
+ Promise.resolve({ hello: 'world' }).then<
883
+ TypedResponse<{ hello: string }, 200, 'json'>
884
+ >((d) => c.json(d, 200))
885
+ )
886
+ ```