toiljs 0.0.85 → 0.0.87

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 (132) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +303 -293
  5. package/build/compiler/.tsbuildinfo +1 -1
  6. package/build/compiler/config.d.ts +2 -0
  7. package/build/compiler/config.js +1 -0
  8. package/build/compiler/docs.js +8 -24
  9. package/build/compiler/generate.js +4 -2
  10. package/build/compiler/index.d.ts +1 -1
  11. package/build/compiler/index.js +69 -6
  12. package/build/compiler/toil-docs.generated.js +64 -22
  13. package/build/devserver/.tsbuildinfo +1 -1
  14. package/build/devserver/analytics/index.js +7 -3
  15. package/build/devserver/db/database.d.ts +4 -0
  16. package/build/devserver/db/database.js +43 -1
  17. package/build/devserver/db/index.d.ts +1 -1
  18. package/build/devserver/db/index.js +1 -1
  19. package/build/devserver/db/types.d.ts +3 -0
  20. package/build/devserver/db/types.js +2 -0
  21. package/build/devserver/runtime/module.js +4 -1
  22. package/docs/README.md +104 -65
  23. package/docs/auth/README.md +102 -0
  24. package/docs/auth/configuration.md +94 -0
  25. package/docs/auth/extending.md +202 -0
  26. package/docs/auth/how-it-works.md +138 -0
  27. package/docs/auth/usage.md +188 -0
  28. package/docs/backend/README.md +143 -0
  29. package/docs/backend/data.md +351 -0
  30. package/docs/backend/rest.md +402 -0
  31. package/docs/backend/rpc.md +226 -0
  32. package/docs/background/README.md +114 -0
  33. package/docs/background/daemons.md +230 -0
  34. package/docs/background/derive.md +179 -0
  35. package/docs/cli/README.md +377 -0
  36. package/docs/concepts/config.md +416 -0
  37. package/docs/concepts/decorators.md +127 -0
  38. package/docs/concepts/security.md +108 -0
  39. package/docs/concepts/tiers.md +166 -0
  40. package/docs/concepts/types.md +216 -0
  41. package/docs/database/README.md +143 -0
  42. package/docs/database/capacity.md +350 -0
  43. package/docs/database/counters.md +174 -0
  44. package/docs/database/documents.md +255 -0
  45. package/docs/database/events.md +307 -0
  46. package/docs/database/membership.md +246 -0
  47. package/docs/database/setup.md +155 -0
  48. package/docs/database/unique.md +216 -0
  49. package/docs/database/views.md +246 -0
  50. package/docs/frontend/README.md +101 -0
  51. package/docs/frontend/data-fetching.md +243 -0
  52. package/docs/frontend/images.md +148 -0
  53. package/docs/frontend/metadata.md +344 -0
  54. package/docs/frontend/rendering.md +236 -0
  55. package/docs/frontend/routing.md +344 -0
  56. package/docs/frontend/scripts.md +118 -0
  57. package/docs/frontend/search.md +191 -0
  58. package/docs/frontend/styling.md +147 -0
  59. package/docs/getting-started/README.md +81 -0
  60. package/docs/getting-started/create-project.md +131 -0
  61. package/docs/getting-started/deploy.md +101 -0
  62. package/docs/getting-started/first-app.md +215 -0
  63. package/docs/getting-started/installation.md +106 -0
  64. package/docs/getting-started/migrating.md +125 -0
  65. package/docs/getting-started/project-structure.md +163 -0
  66. package/docs/introduction/README.md +41 -0
  67. package/docs/introduction/design-principles.md +55 -0
  68. package/docs/introduction/distributed.md +74 -0
  69. package/docs/introduction/how-it-works.md +74 -0
  70. package/docs/introduction/hyperscale.md +51 -0
  71. package/docs/introduction/modern-stack.md +36 -0
  72. package/docs/introduction/vs-other-frameworks.md +48 -0
  73. package/docs/introduction/why-toil.md +93 -0
  74. package/docs/llms.txt +90 -0
  75. package/docs/realtime/README.md +102 -0
  76. package/docs/realtime/channels.md +211 -0
  77. package/docs/realtime/streams.md +369 -0
  78. package/docs/services/README.md +60 -0
  79. package/docs/services/analytics.md +268 -0
  80. package/docs/services/caching.md +175 -0
  81. package/docs/services/cookies.md +235 -0
  82. package/docs/services/crypto.md +209 -0
  83. package/docs/services/email.md +289 -0
  84. package/docs/services/environment.md +141 -0
  85. package/docs/services/ratelimit.md +174 -0
  86. package/docs/services/time.md +85 -0
  87. package/examples/basic/client/routes/analytics.tsx +13 -12
  88. package/examples/basic/server/models/SiteAnalytics.ts +29 -17
  89. package/examples/basic/server/routes/Analytics.ts +15 -17
  90. package/examples/basic/server/routes/UserId.ts +22 -0
  91. package/package.json +15 -11
  92. package/scripts/gen-toil-docs.mjs +24 -35
  93. package/server/auth/AuthController.ts +336 -0
  94. package/server/auth/AuthUser.ts +23 -0
  95. package/server/auth/index.ts +16 -0
  96. package/server/globals/auth.ts +31 -0
  97. package/server/globals/userid.ts +107 -0
  98. package/src/compiler/config.ts +13 -0
  99. package/src/compiler/docs.ts +16 -33
  100. package/src/compiler/generate.ts +6 -2
  101. package/src/compiler/index.ts +114 -6
  102. package/src/compiler/toil-docs.generated.ts +64 -22
  103. package/src/devserver/analytics/index.ts +10 -4
  104. package/src/devserver/db/database.ts +67 -1
  105. package/src/devserver/db/index.ts +1 -0
  106. package/src/devserver/db/types.ts +13 -0
  107. package/src/devserver/runtime/module.ts +7 -0
  108. package/test/analytics-dev.test.ts +2 -1
  109. package/test/devserver-database.test.ts +113 -0
  110. package/docs/auth-todo.md +0 -149
  111. package/docs/auth.md +0 -322
  112. package/docs/caching.md +0 -115
  113. package/docs/cli.md +0 -17
  114. package/docs/client.md +0 -39
  115. package/docs/cookies.md +0 -457
  116. package/docs/crypto.md +0 -130
  117. package/docs/daemon.md +0 -123
  118. package/docs/data.md +0 -131
  119. package/docs/derive.md +0 -159
  120. package/docs/email.md +0 -326
  121. package/docs/environment.md +0 -97
  122. package/docs/getting-started.md +0 -128
  123. package/docs/index.md +0 -30
  124. package/docs/ratelimit.md +0 -95
  125. package/docs/routing.md +0 -259
  126. package/docs/rpc.md +0 -149
  127. package/docs/server.md +0 -61
  128. package/docs/ssr.md +0 -632
  129. package/docs/streams.md +0 -178
  130. package/docs/styling.md +0 -22
  131. package/docs/tiers.md +0 -133
  132. package/docs/time.md +0 -43
@@ -0,0 +1,402 @@
1
+ # HTTP routes (`@rest`)
2
+
3
+ Expose a real HTTP API by tagging a class `@rest` and its methods with an HTTP verb; toiljs generates the router for you.
4
+
5
+ ## Why and when
6
+
7
+ Use `@rest` when something outside your own frontend needs to call your backend over plain HTTP: a webhook from another service, a public API, a mobile app, a `curl` script, or a browser hitting a URL directly. REST endpoints are ordinary URLs with ordinary HTTP methods and status codes, so any HTTP client understands them.
8
+
9
+ If instead you are calling your own backend from your own React frontend, [typed RPC](./rpc.md) is usually nicer (you get end-to-end types and skip URL wrangling). The two live happily side by side.
10
+
11
+ ## The shape of a route
12
+
13
+ A REST route is a method on a class. The class decorator says where the class is mounted; the method decorator says which HTTP method and path it answers.
14
+
15
+ ```ts
16
+ import { Response, RouteContext } from 'toiljs/server/runtime';
17
+
18
+ @rest('players') // this controller is mounted at /players
19
+ class Players {
20
+ @get('/:id') // answers GET /players/:id
21
+ public get(ctx: RouteContext): Response {
22
+ const id = ctx.param('id');
23
+ return Response.json(`{"id":"${id}"}`);
24
+ }
25
+ }
26
+ ```
27
+
28
+ Save that file, import it once (see [How routes are discovered](#how-routes-are-discovered)), and `GET /players/42` returns `{"id":"42"}`. That is a working endpoint.
29
+
30
+ ## `@rest`: mounting a controller
31
+
32
+ `@rest` marks a class as a route **controller** (a group of related routes) and mounts it at a URL prefix.
33
+
34
+ ```ts
35
+ @rest('api') // mounted at /api
36
+ @rest('/') // or @rest('') : mounted at the root
37
+ @rest({ stream: DataStream.Binary }) // root mount, binary body codec by default
38
+ ```
39
+
40
+ - The string is the mount prefix. `"api"`, `"/api"`, and `"api/"` all normalize to `/api`. `""` and `"/"` mean the root.
41
+ - The object form sets class-wide defaults. `stream: DataStream.Binary` makes every route in the class use the binary body codec instead of JSON (more on that in [Request and response bodies](#request-and-response-bodies)).
42
+
43
+ The full URL of a route is the controller prefix joined with the method path. With `@rest('api')` and `@get('/todos/:id')`, the route is `GET /api/todos/:id`.
44
+
45
+ ## Verb decorators
46
+
47
+ Each HTTP method has a decorator that takes the route path:
48
+
49
+ ```ts
50
+ @get('/path') @post('/path') @put('/path') @patch('/path')
51
+ @del('/path') @head('/path') @options('/path')
52
+ ```
53
+
54
+ Note the delete decorator is `@del`, not `@delete` (the word `delete` is reserved in TypeScript, so it cannot be a decorator name).
55
+
56
+ ### `@route`: the explicit form
57
+
58
+ `@route` is the general form. Reach for it when you want to set the body codec per route, or you just prefer an object:
59
+
60
+ ```ts
61
+ @route({ method: Methods.POST, path: '/upload', stream: DataStream.Binary })
62
+ public upload(body: FileData): FileResult { /* ... */ }
63
+ ```
64
+
65
+ `method` (from the `Methods` enum) and `path` are required; `stream` is optional and overrides the controller default. `Methods` and `DataStream` are global enums (like the decorators themselves, they need no import).
66
+
67
+ ## Path parameters
68
+
69
+ A path segment written as `:name` is a **path parameter**: it captures whatever the request has in that position. Read it with `ctx.param('name')`.
70
+
71
+ ```ts
72
+ @get('/todos/:id/items/:itemId')
73
+ public getItem(ctx: RouteContext): Response {
74
+ const id = ctx.param('id'); // "42" for /todos/42/items/9
75
+ const itemId = ctx.param('itemId'); // "9"
76
+ return Response.json(`{"todo":"${id}","item":"${itemId}"}`);
77
+ }
78
+ ```
79
+
80
+ Matching is **segment-exact**: the request path must have the same number of `/`-separated segments, the static segments must match literally, and each `:param` captures one segment. The query string is removed before matching. A captured param is always a string; convert it yourself if you need a number (for example `u64.parse(ctx.param('id'))`).
81
+
82
+ ## Method parameters: reading the request
83
+
84
+ A route method may declare zero, one, or two parameters. toiljs looks at their types to decide what to pass:
85
+
86
+ - a parameter typed `RouteContext` receives the [request context](#the-routecontext-object) (path params, query, headers, raw body);
87
+ - any other type is treated as the **request body**, decoded into that [`@data`](./data.md) type.
88
+
89
+ ```ts
90
+ @get('/status')
91
+ public status(): StatusResponse { /* no body, no context */ }
92
+
93
+ @get('/user/:id')
94
+ public getUser(ctx: RouteContext): User { /* context only */ }
95
+
96
+ @post('/create')
97
+ public create(input: NewTodo): Todo { /* body only */ }
98
+
99
+ @post('/user/:id/score')
100
+ public addScore(input: ScoreDelta, ctx: RouteContext): Player {
101
+ const id = ctx.param('id'); // body AND context
102
+ /* ... */
103
+ }
104
+ ```
105
+
106
+ The order of the two parameters does not matter; toiljs classifies them by type.
107
+
108
+ ## Return types
109
+
110
+ You return one of two things from a route, and toiljs encodes it for you:
111
+
112
+ | You return | toiljs sends |
113
+ | --- | --- |
114
+ | a [`@data`](./data.md) value | The value serialized (JSON by default, binary if the route is in binary mode). Status `200`. |
115
+ | a `Response` | Exactly that response: your status, headers, cookies, and body, untouched. |
116
+ | `void` (nothing) | `204 No Content`. |
117
+
118
+ Returning a `@data` value is the short path when you just want to send the data. Returning a `Response` gives you full control: a custom status, extra headers, cookies, caching. Use whichever fits.
119
+
120
+ ```ts
121
+ // Short path: return the data, let toiljs serialize it as JSON.
122
+ @get('/me')
123
+ public me(): Player { return currentPlayer(); }
124
+
125
+ // Full control: custom 404, a header, and the body serialized by hand.
126
+ @get('/:id')
127
+ public get(ctx: RouteContext): Response {
128
+ const id = u64.parse(ctx.param('id'));
129
+ if (!store.has(id)) return Response.notFound();
130
+ return Response.json(store.get(id).toJSON().toString())
131
+ .setHeader('cache-control', 'no-store');
132
+ }
133
+ ```
134
+
135
+ ## Request and response bodies
136
+
137
+ Every route is either a **JSON route** (the default) or a **Binary route**. This decides how request bodies are decoded and response values are encoded:
138
+
139
+ - **JSON**: the request body is parsed as JSON and revived into your `@data` type; the response value is serialized to JSON. Best for endpoints a browser or a third party calls directly, because JSON is universally readable.
140
+ - **Binary**: the request body and response use toiljs's compact binary codec (`DataWriter`/`DataReader`). Smaller and faster, and exact for very large integers. Best for app-to-app traffic and anything performance sensitive.
141
+
142
+ Set the mode on the whole controller with `@rest({ stream: DataStream.Binary })`, or per route with `@route({ ..., stream: DataStream.Binary })`. Full detail on both codecs is in [Data types](./data.md).
143
+
144
+ > Large integers and JSON: numbers of 64 bits or more (`u64`, `i64`, `u128`, `u256`, and friends) are sent over JSON as decimal strings, so they stay exact at any size (plain JSON numbers would lose precision). The generated client turns them back into `bigint`. See [Types](../concepts/types.md).
145
+
146
+ ## The `RouteContext` object
147
+
148
+ `RouteContext` is your window into the incoming request. toiljs builds one and passes it to any route method that declares a `RouteContext` parameter.
149
+
150
+ | Member | Signature | What it gives you |
151
+ | --- | --- | --- |
152
+ | `request` | `Request` | The raw request (method, path, headers, body). |
153
+ | `param` | `param(name: string): string` | A captured path param, or `""` if absent. |
154
+ | `query` | `query(name: string): string` | A query-string value (`?q=hi`), or `""` if absent. Not URL-decoded in v1. |
155
+ | `header` | `header(name: string): string \| null` | A request header, case-insensitive, or `null`. |
156
+ | `text` | `text(): string` | The raw request body decoded as UTF-8 text. |
157
+ | `clientIp` | `clientIp(): string` | The connecting client's IP, or `""` if unavailable. |
158
+
159
+ `clientIp()` is the real socket address the edge observed, not a forgeable header like `X-Forwarded-For`, so it is safe to key rate limits, geo lookups, or audit logs on it.
160
+
161
+ ## The `Request` object
162
+
163
+ The raw request. You get it as `ctx.request`, or directly as the argument to a hand-written `handle(req)`.
164
+
165
+ **Fields:**
166
+
167
+ | Field | Type | Notes |
168
+ | --- | --- | --- |
169
+ | `method` | `Method` | `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, or `UNKNOWN`. |
170
+ | `path` | `string` | The path, including the query string. |
171
+ | `headers` | `Array<Header>` | Ordered list; a `Header` is `{ name, value }`. |
172
+ | `body` | `Uint8Array` | The raw request body bytes. |
173
+
174
+ **Methods:**
175
+
176
+ | Method | Signature | Notes |
177
+ | --- | --- | --- |
178
+ | `header` | `header(name: string): string \| null` | Case-insensitive lookup, `null` if absent. |
179
+ | `cookies` | `cookies(): CookieMap` | Parses the `Cookie` header (values percent-decoded); cached per request. |
180
+ | `cookie` | `cookie(name: string): string \| null` | A single cookie value, or `null`. |
181
+
182
+ The `Method` enum and the `Header` class are exported from `toiljs/server/runtime`.
183
+
184
+ ## Building a response
185
+
186
+ `Response` is what you return. Create one with a static factory, then chain instance methods to add headers, cookies, and caching. Every instance method returns the same `Response`, so calls chain.
187
+
188
+ ### Static factories
189
+
190
+ | Factory | Signature | Status | Content-Type |
191
+ | --- | --- | --- | --- |
192
+ | `Response.text` | `text(body: string, status = 200)` | 200 | `text/plain; charset=utf-8` |
193
+ | `Response.html` | `html(body: string, status = 200)` | 200 | `text/html; charset=utf-8` |
194
+ | `Response.json` | `json(body: string, status = 200)` | 200 | `application/json; charset=utf-8` |
195
+ | `Response.bytes` | `bytes(body: Uint8Array, status = 200)` | 200 | `application/octet-stream` |
196
+ | `Response.empty` | `empty(status)` | custom | (none) |
197
+ | `Response.notFound` | `notFound()` | 404 | text |
198
+ | `Response.badRequest` | `badRequest(msg = 'bad request')` | 400 | text |
199
+ | `Response.internalError` | `internalError(msg = 'internal error')` | 500 | text |
200
+ | `Response.unhandled` | `unhandled()` | 404 | text, plus a marker header (see below) |
201
+
202
+ `Response.json` takes an **already-serialized** string, not an object. Build it from a `@data` value with `value.toJSON().toString()`, or return the `@data` value directly and let toiljs serialize it (usually simpler).
203
+
204
+ ### Instance methods
205
+
206
+ | Method | Signature | What it does |
207
+ | --- | --- | --- |
208
+ | `setHeader` | `setHeader(name, value): Response` | Appends a header (call again to add more). |
209
+ | `setCookie` | `setCookie(cookie: Cookie): Response` | Appends a `Set-Cookie`. |
210
+ | `setCookieKV` | `setCookieKV(name, value): Response` | Shorthand for a cookie with no attributes. |
211
+ | `clearCookie` | `clearCookie(name, path = '/', domain = ''): Response` | Emits a deletion cookie (empty value, `Max-Age=0`). |
212
+ | `cache` | `cache(edgeTtlMinutes, browserTtlSeconds = 0, privateScope = false, allowAuth = false): Response` | Marks the response cacheable. See [Caching](../services/caching.md). |
213
+ | `cacheFor` | `cacheFor(minutes): Response` | Shorthand for edge-caching for N minutes. |
214
+
215
+ ```ts
216
+ return Response.json('{"id":42}')
217
+ .setHeader('x-trace', traceId)
218
+ .setCookieKV('seen', '1')
219
+ .cacheFor(5);
220
+ ```
221
+
222
+ See [Cookies](../services/cookies.md) for the full cookie builder and [Caching](../services/caching.md) for the caching rules.
223
+
224
+ ## How dispatch works
225
+
226
+ Every `@rest` controller registers itself into a global `Rest` registry when its module loads. At request time, `Rest.dispatch(req)` walks the controllers and tries their routes.
227
+
228
+ ```mermaid
229
+ flowchart TD
230
+ A["Request arrives"] --> B["Rest.dispatch(req)"]
231
+ B --> C{"Any controller<br/>route match<br/>the method + path?"}
232
+ C -->|"Yes"| D["Run that method"]
233
+ D --> E["Return its Response"]
234
+ C -->|"No"| F["dispatch returns null"]
235
+ F --> G["Your fallback logic<br/>(or Response.unhandled)"]
236
+ ```
237
+
238
+ Matching order is deterministic: controllers are tried in the order their modules load, and routes within a controller in declaration order. The **first match wins**, so put more specific routes before catch-all ones.
239
+
240
+ ## How routes are discovered
241
+
242
+ `toiljs build` scans every file under `server/` and finds your decorated classes on its own, so a route file is picked up as soon as it exists. To keep a plain `toilscript` run (and your editor) seeing the same set, projects also `import` each route file once in `server/main.ts`:
243
+
244
+ ```ts
245
+ // server/main.ts (excerpt)
246
+ import './routes/Players';
247
+ import './routes/Todos';
248
+ ```
249
+
250
+ Adding a new controller is two steps: write the file, add the one-line import.
251
+
252
+ ## Dispatch and the 404 fallback
253
+
254
+ Inside your handler, the usual pattern is: try REST first, then fall through to anything else.
255
+
256
+ ```ts
257
+ const hit = Rest.dispatch(req); // Response, or null if nothing matched
258
+ if (hit != null) return hit;
259
+ return Response.unhandled(); // no route matched here
260
+ ```
261
+
262
+ There are two different 404s, and the difference matters:
263
+
264
+ - **`Response.notFound()`** means "I looked, and that resource does not exist." It is sent to the client as a plain `404`.
265
+ - **`Response.unhandled()`** means "this server has no route for that path." It is a `404` carrying a marker header (`x-toil-unhandled`). The edge (and the dev server) reads that marker and tries to serve the path another way: a static file, or the client-side app. The marker is stripped before anything reaches the browser.
266
+
267
+ Rule of thumb: return `unhandled()` when a path is simply not yours to handle, and `notFound()` when the path is yours but the specific thing is missing.
268
+
269
+ If your project is REST-only, you do not need a custom handler at all. toiljs ships `RestHandler`, which does exactly the dispatch-then-`unhandled` dance:
270
+
271
+ ```ts
272
+ import { Server, RestHandler } from 'toiljs/server/runtime';
273
+ Server.handler = () => new RestHandler();
274
+ ```
275
+
276
+ ## Guards: auth, rate limits, caching
277
+
278
+ You can stack extra decorators on a route (or a whole controller) to protect or cache it. They compose with the verb decorators:
279
+
280
+ ```ts
281
+ @rest('admin')
282
+ class Admin {
283
+ @get('/stats')
284
+ @auth // reject with 401 if there is no valid session
285
+ @ratelimit(RateLimit.SlidingWindow, 30, 60) // at most 30 requests per 60 seconds
286
+ public stats(): Stats { /* ... */ }
287
+ }
288
+ ```
289
+
290
+ - `@auth` requires a valid signed session, else the request is rejected with `401` before your method runs. See the [Auth guide](../auth/README.md).
291
+ - `@ratelimit` caps how often a caller may hit the route. See [Rate limiting](../services/ratelimit.md).
292
+ - Response caching is opt-in per response with `.cache(...)` / `.cacheFor(...)`. See [Caching](../services/caching.md).
293
+
294
+ ## A complete CRUD example
295
+
296
+ A small in-memory players API showing create, read, update, and delete. In a real app you would store players in [ToilDB](../database/README.md) instead of a local map, because each request gets a fresh handler (see [statelessness](./README.md#stateless-by-default)); this example keeps it in memory to stay focused on routing.
297
+
298
+ ```ts
299
+ // server/models/NewPlayer.ts
300
+ @data
301
+ export class NewPlayer {
302
+ name: string = '';
303
+ }
304
+ ```
305
+
306
+ ```ts
307
+ // server/models/Player.ts
308
+ @data
309
+ export class Player {
310
+ id: u64 = 0;
311
+ name: string = '';
312
+ score: i64 = 0;
313
+ }
314
+ ```
315
+
316
+ ```ts
317
+ // server/routes/Players.ts
318
+ import { Response, RouteContext } from 'toiljs/server/runtime';
319
+ import { NewPlayer } from '../models/NewPlayer';
320
+ import { Player } from '../models/Player';
321
+
322
+ // A stand-in store. Real apps use ToilDB (see the database section).
323
+ const store = new Map<u64, Player>();
324
+ let nextId: u64 = 1;
325
+
326
+ @rest('players')
327
+ class Players {
328
+ // CREATE: POST /players (JSON body -> Player)
329
+ @post('/')
330
+ public create(input: NewPlayer): Player {
331
+ const p = new Player();
332
+ p.id = nextId++;
333
+ p.name = input.name;
334
+ p.score = 0;
335
+ store.set(p.id, p);
336
+ return p; // 200 with the player as JSON
337
+ }
338
+
339
+ // READ: GET /players/:id
340
+ @get('/:id')
341
+ public get(ctx: RouteContext): Response {
342
+ const id = u64.parse(ctx.param('id'));
343
+ if (!store.has(id)) return Response.notFound();
344
+ return Response.json(store.get(id).toJSON().toString());
345
+ }
346
+
347
+ // UPDATE: PUT /players/:id (JSON body -> Player)
348
+ @put('/:id')
349
+ public update(input: NewPlayer, ctx: RouteContext): Response {
350
+ const id = u64.parse(ctx.param('id'));
351
+ if (!store.has(id)) return Response.notFound();
352
+ const p = store.get(id);
353
+ p.name = input.name;
354
+ return Response.json(p.toJSON().toString());
355
+ }
356
+
357
+ // DELETE: DELETE /players/:id
358
+ @del('/:id')
359
+ public remove(ctx: RouteContext): Response {
360
+ const id = u64.parse(ctx.param('id'));
361
+ if (!store.has(id)) return Response.notFound();
362
+ store.delete(id);
363
+ return Response.empty(204); // 204 No Content
364
+ }
365
+ }
366
+ ```
367
+
368
+ Calling it from anywhere:
369
+
370
+ ```sh
371
+ curl -X POST localhost:5173/players -d '{"name":"Ada"}'
372
+ curl localhost:5173/players/1
373
+ curl -X PUT localhost:5173/players/1 -d '{"name":"Ada Lovelace"}'
374
+ curl -X DELETE localhost:5173/players/1
375
+ ```
376
+
377
+ Because `@rest` also generates a typed fetch client, your React frontend can call the same routes without writing URLs:
378
+
379
+ ```ts
380
+ await Server.REST.players.create({ body: new NewPlayer('Ada') });
381
+ await Server.REST.players.get({ params: { id: 1 } });
382
+ ```
383
+
384
+ See [RPC and the generated client](./rpc.md#the-rest-fetch-client) for that client.
385
+
386
+ ## Gotchas
387
+
388
+ - **Fields do not persist.** A fresh controller instance serves each request, so instance fields reset every time and are never shared between requests or edge nodes. Persist to [ToilDB](../database/README.md).
389
+ - **`Response.json` wants a string.** Pass an already-serialized JSON string (or return the `@data` value directly). Passing an object will not do what you expect.
390
+ - **`@del`, not `@delete`.** `delete` is a reserved word, so the decorator is `@del`.
391
+ - **Query values are not URL-decoded in v1.** `ctx.query('q')` returns the raw value; decode it yourself if it may contain percent-encoding.
392
+ - **Path matching is exact on segment count.** `/todos/:id` does not match `/todos/1/extra`. Add a route for the longer path.
393
+ - **`notFound()` vs `unhandled()`.** Returning `notFound()` from your top handler stops the edge from falling through to static files or the client app. Use `unhandled()` for "not my path."
394
+
395
+ ## Related
396
+
397
+ - [Data types (`@data`)](./data.md): the request and response body structs, and the JSON vs binary codecs.
398
+ - [Typed RPC](./rpc.md): call your backend from your frontend with end-to-end types (and the generated REST fetch client).
399
+ - [Backend overview](./README.md): the request lifecycle and handler model.
400
+ - [Cookies](../services/cookies.md), [Caching](../services/caching.md), [Rate limiting](../services/ratelimit.md): response helpers and guards.
401
+ - [Auth](../auth/README.md): protecting routes with `@auth`.
402
+ - [The database](../database/README.md): persisting data between requests.
@@ -0,0 +1,226 @@
1
+ # Typed RPC (`@service` / `@remote`)
2
+
3
+ Write a server function, tag it `@remote`, and call it from your React frontend like a local async function, with the argument and return types checked end to end.
4
+
5
+ ## What RPC is
6
+
7
+ **RPC** stands for Remote Procedure Call. The idea is old and simple: call a function that actually runs somewhere else (here, on the edge) as if it were a normal function in your own code. You write a method on the server, and on the client you `await` it. No URLs, no `fetch`, no manual JSON. Just a function call.
8
+
9
+ toiljs makes that call **fully typed**. When you build the server, it generates a TypeScript file (`shared/server.ts`) describing every callable function, its arguments, and its return type. Your frontend imports nothing extra: a global object called `Server` is available with all of it typed. If you change a server function's signature and rebuild, your frontend code stops type-checking until you fix the call. The client can never drift from the server.
10
+
11
+ ## Why and when
12
+
13
+ Use RPC when **your own frontend** talks to **your own backend**. It is the most ergonomic and safest way to do that:
14
+
15
+ - End-to-end types: rename a field on the server, and the client sees it immediately.
16
+ - No plumbing: no route paths to invent, no request or response shapes to hand-write.
17
+ - Exact numbers: 64-bit-and-larger integers arrive as `bigint`, never rounded.
18
+
19
+ Use [`@rest`](./rest.md) instead when the caller is **not** your frontend: a webhook, a third-party integration, a public API, a mobile client, or anything that speaks in plain URLs and HTTP methods. RPC uses one internal endpoint and a binary wire format, so it is not meant to be called by hand.
20
+
21
+ You can use both in the same project. A common split: RPC for your app's own screens, REST for everything public.
22
+
23
+ ## Declaring callable functions
24
+
25
+ Two decorators expose server code to the client:
26
+
27
+ - **`@remote`** on a top-level function makes it directly callable.
28
+ - **`@service`** on a class groups related `@remote` methods under a namespace.
29
+
30
+ ```ts
31
+ // server/services/Stats.ts
32
+ import { store } from '../core/store';
33
+
34
+ @service
35
+ class Stats {
36
+ @remote
37
+ public playerCount(): i32 {
38
+ return store.size;
39
+ }
40
+ }
41
+ ```
42
+
43
+ ```ts
44
+ // server/services/remotes.ts
45
+ @remote
46
+ function ping(n: i32): i32 {
47
+ return n + 1;
48
+ }
49
+ ```
50
+
51
+ That is the whole server side. Build the server and the client can call them.
52
+
53
+ ## Calling from the frontend
54
+
55
+ The generated client surfaces everything on a global `Server`. A `@service` becomes a namespace keyed by the class name with a lowercase first letter (`Stats` becomes `stats`). A free `@remote` sits directly on `Server`. Every call returns a `Promise`.
56
+
57
+ ```ts
58
+ // anywhere in your React app, no import needed
59
+ const count = await Server.stats.playerCount(); // number
60
+ const next = await Server.ping(41); // number -> 42
61
+ ```
62
+
63
+ Autocomplete, argument checking, and the return type all come from `shared/server.ts`, which the build regenerates every time (and `toiljs dev` regenerates on save), so it is always in sync.
64
+
65
+ ## The round trip
66
+
67
+ Here is what actually happens under that innocent-looking `await`.
68
+
69
+ ```mermaid
70
+ sequenceDiagram
71
+ participant C as Browser (Server.stats.playerCount)
72
+ participant E as Dacely edge
73
+ participant W as Your backend (WASM)
74
+ C->>C: encode args with DataWriter
75
+ C->>E: POST /__toil_rpc<br/>header dacely-rpc = method id<br/>body = encoded args
76
+ E->>W: dispatch to the matching @remote
77
+ W->>W: run playerCount(), encode the result
78
+ W->>E: encoded result bytes
79
+ E->>C: 200 application/octet-stream
80
+ C->>C: decode into the typed value
81
+ ```
82
+
83
+ A few facts worth knowing:
84
+
85
+ - Every callable has a stable numeric **method id** (a hash of `"Service.method"` or the function name). The client sends it in the `dacely-rpc` header; the server dispatches on it.
86
+ - Arguments and results travel in the compact **binary `@data` codec** (see [Data types](./data.md)), so large integers are exact and payloads are small.
87
+ - The endpoint is a single reserved path, `/__toil_rpc`. You never route it yourself; the framework handles it before your `handle` runs.
88
+
89
+ ## RPC is stateless too
90
+
91
+ Just like a REST controller, a fresh service instance serves each call. Fields you set on a `@service` class do not survive between calls. If two calls need to share data, that data lives in [ToilDB](../database/README.md), not in an instance field. See [statelessness](./README.md#stateless-by-default).
92
+
93
+ ## Argument and return types
94
+
95
+ Arguments and return values may be scalars, arrays, or [`@data`](./data.md) classes, in both directions. Here is how toilscript's types map to what you see on the TypeScript client:
96
+
97
+ | ToilScript type | TypeScript type |
98
+ | --- | --- |
99
+ | `u8`, `u16`, `u32`, `i8`, `i16`, `i32`, `f32`, `f64` | `number` |
100
+ | `u64`, `i64`, `u128`, `i128`, `u256`, `i256` | `bigint` |
101
+ | `bool` | `boolean` |
102
+ | `string` | `string` |
103
+ | a `@data` class `T` | `T` (the generated class) |
104
+ | `T[]` | `T[]` |
105
+
106
+ Integers of 64 bits or more become `bigint` on the client, so they are exact at any magnitude. See [Types](../concepts/types.md) for the full number story.
107
+
108
+ Passing a `@data` value is just as easy as a scalar. Construct it on the client and pass it in:
109
+
110
+ ```ts
111
+ import { NewPlayer } from './shared/server';
112
+
113
+ const created = await Server.roster.add(new NewPlayer('Ada')); // returns a typed Player
114
+ ```
115
+
116
+ The `@data` classes in `shared/server.ts` share a byte-for-byte identical codec with the server, so values round-trip exactly.
117
+
118
+ ## Reading and writing the database from a `@remote`
119
+
120
+ A `@remote` can use the database, but with a safety default: **a plain `@remote` is read-only.** If it tries to write to ToilDB, the compiler rejects it. To let a `@remote` write, add `@action`:
121
+
122
+ ```ts
123
+ @service
124
+ class Roster {
125
+ @remote
126
+ public count(): i32 { // read-only: fine to just read
127
+ return db.players.count();
128
+ }
129
+
130
+ @remote
131
+ @action // opts into writes
132
+ public add(input: NewPlayer): Player {
133
+ return db.players.create(/* ... */);
134
+ }
135
+ }
136
+ ```
137
+
138
+ `@query` is the explicit opposite of `@action`: it marks a function read-only on purpose (the default for a `@remote`, so you rarely need to write it). These are ToilDB **function kinds**; the full rules, including what each kind may and may not do, are in the [database docs](../database/README.md). The takeaway for RPC: reads work out of the box, and a write needs `@action`, so a read-only endpoint can never silently mutate your data.
139
+
140
+ ## Guarding a `@remote`
141
+
142
+ Guards stack on a `@remote` exactly as they do on a REST route:
143
+
144
+ ```ts
145
+ @service
146
+ class Stats {
147
+ @remote
148
+ @auth // reject with 401 when there is no valid session
149
+ public secretCount(): i32 {
150
+ return store.size;
151
+ }
152
+ }
153
+ ```
154
+
155
+ The RPC dispatcher enforces `@auth` (and `@ratelimit`) the same way the REST router does: the guard runs first, and an unauthenticated call gets a `401` before your method body executes. See the [Auth guide](../auth/README.md) and [Rate limiting](../services/ratelimit.md).
156
+
157
+ ## The generated `Server` surface
158
+
159
+ `shared/server.ts` declares `Server` as a global with a shape like this (schematic):
160
+
161
+ ```ts
162
+ declare global {
163
+ const Server: {
164
+ // free @remote functions
165
+ ping(n: number): Promise<number>;
166
+
167
+ // @service classes, keyed by lowercased name
168
+ readonly stats: {
169
+ playerCount(): Promise<number>;
170
+ secretCount(): Promise<number>;
171
+ };
172
+
173
+ // @rest controllers get a fetch client under REST (see below)
174
+ readonly REST: { /* ... */ };
175
+
176
+ // @stream classes get a client under Stream (see the realtime docs)
177
+ readonly Stream: { /* ... */ };
178
+ };
179
+ }
180
+ ```
181
+
182
+ You never edit this file; the build regenerates it. If a `Server` method throws that it is "unavailable," the generated client has not loaded yet: run the server build (or `toiljs dev`, which does it on save).
183
+
184
+ ## The REST fetch client
185
+
186
+ `@rest` controllers also get a typed client, under `Server.REST.<controller>.<route>`. It is real `fetch` code (because REST is just HTTP), and it is handy when you want your frontend to call a route you also expose publicly:
187
+
188
+ ```ts
189
+ // controller @rest('players') with a create route taking a NewPlayer body
190
+ const player = await Server.REST.players.create({
191
+ body: new NewPlayer('Ada'), // present only if the route takes a body
192
+ // params: { id: 7 }, // present only if the path has :params
193
+ query: { ref: 'home' }, // optional
194
+ headers: { 'x-trace': id }, // optional
195
+ });
196
+ ```
197
+
198
+ The wrapper builds the URL, substitutes `:params`, appends `query`, sends the request, throws on a non-2xx status, and decodes the response into the route's return type. A route declared to return `Response` resolves to the raw `fetch` `Response` so you can inspect headers or stream it yourself. See [REST](./rest.md) for the routes themselves.
199
+
200
+ ## RPC vs REST at a glance
201
+
202
+ | | RPC (`@service` / `@remote`) | REST (`@rest`) |
203
+ | --- | --- | --- |
204
+ | Caller | Your own frontend | Anyone (browser, webhook, third party, `curl`) |
205
+ | Client | `Server.svc.method(args)` | `Server.REST.ctrl.route(args)`, or plain `fetch` |
206
+ | URL shape | One internal endpoint | Real paths and HTTP methods you design |
207
+ | Wire format | Compact binary `@data` | JSON (or binary), your choice |
208
+ | Types | End to end, automatic | End to end for the generated client |
209
+ | Best for | App-internal calls | Public APIs, integrations, webhooks |
210
+
211
+ ## Gotchas
212
+
213
+ - **RPC is not a public API.** It uses one reserved endpoint and a binary format meant for the generated client. If an outside system needs to call in, expose a [`@rest`](./rest.md) route.
214
+ - **Instance fields do not persist.** A fresh service instance serves every call. Shared state belongs in [ToilDB](../database/README.md).
215
+ - **Writes need `@action`.** A plain `@remote` is read-only; the compiler rejects a database write unless the method is `@action`.
216
+ - **Rebuild after signature changes.** `shared/server.ts` is generated. If autocomplete looks stale, rebuild the server (`toiljs dev` does this on save).
217
+ - **`bigint`, not `number`, for 64-bit values.** A `u64`/`i64`/`u256` argument or return is a `bigint` on the client. Pass `10n`, not `10`.
218
+
219
+ ## Related
220
+
221
+ - [Data types (`@data`)](./data.md): the structs your RPC arguments and results are made of, and the binary codec they travel in.
222
+ - [HTTP routes (`@rest`)](./rest.md): the public-facing alternative, and the `Server.REST` fetch client.
223
+ - [Types](../concepts/types.md): `u64`, `u256`, and how they map to `number` / `bigint`.
224
+ - [The database](../database/README.md): `@action` vs `@query`, and persisting state.
225
+ - [Fetching data on the frontend](../frontend/data-fetching.md): using `Server.*` from your React components.
226
+ - [Auth](../auth/README.md): guarding a `@remote` with `@auth`.