@thednp/rpc 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/AGENTS.md +5 -5
  2. package/CLAUDE.md +1 -0
  3. package/README.md +193 -47
  4. package/dist/express/express.d.mts +110 -16
  5. package/dist/express/express.d.mts.map +1 -1
  6. package/dist/express/express.mjs +113 -20
  7. package/dist/express/express.mjs.map +1 -1
  8. package/dist/fastify/fastify.d.mts +66 -1
  9. package/dist/fastify/fastify.d.mts.map +1 -1
  10. package/dist/fastify/fastify.mjs +89 -18
  11. package/dist/fastify/fastify.mjs.map +1 -1
  12. package/dist/fastify/plugin/fastify/plugin.d.mts +64 -9
  13. package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
  14. package/dist/fastify/plugin/fastify/plugin.mjs +78 -18
  15. package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
  16. package/dist/helpers/helpers.d.mts +64 -3
  17. package/dist/helpers/helpers.d.mts.map +1 -1
  18. package/dist/helpers/helpers.mjs +34 -7
  19. package/dist/helpers/helpers.mjs.map +1 -1
  20. package/dist/hono/hono.d.mts +66 -5
  21. package/dist/hono/hono.d.mts.map +1 -1
  22. package/dist/hono/hono.mjs +84 -24
  23. package/dist/hono/hono.mjs.map +1 -1
  24. package/dist/index.d.mts +182 -18
  25. package/dist/index.d.mts.map +1 -1
  26. package/dist/index.mjs +137 -31
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/koa/koa.d.mts +52 -0
  29. package/dist/koa/koa.d.mts.map +1 -1
  30. package/dist/koa/koa.mjs +86 -16
  31. package/dist/koa/koa.mjs.map +1 -1
  32. package/dist/server/server.d.mts +104 -4
  33. package/dist/server/server.d.mts.map +1 -1
  34. package/dist/server/server.mjs +117 -17
  35. package/dist/server/server.mjs.map +1 -1
  36. package/package.json +48 -30
  37. package/wiki/adapters.md +31 -2
  38. package/wiki/best-practices.md +172 -9
  39. package/wiki/client-usage.md +7 -3
  40. package/wiki/configuration.md +5 -6
  41. package/wiki/getting-started.md +35 -6
  42. package/wiki/index.md +8 -20
  43. package/wiki/security.md +27 -5
  44. package/wiki/server-functions.md +49 -5
  45. package/wiki/setup.md +8 -4
@@ -2,12 +2,14 @@
2
2
 
3
3
  ## @thednp/rpc is a Transport Pipe
4
4
 
5
- `@thednp/rpc` handles serialization and **transport only**. It does not provide:
5
+ The best thing to do **first** is to understand `@thednp/rpc` is that it only handles **transport** and **serialization**.
6
6
 
7
+ It does not provide:
7
8
  - Caching
8
9
  - Authentication
9
10
  - Validation
10
11
  - State management
12
+ - Request limits
11
13
  - Retry logic
12
14
 
13
15
  Use dedicated tools for these concerns.
@@ -85,23 +87,107 @@ export const updateProfile = createServerFunction(
85
87
  );
86
88
  ```
87
89
 
88
- Return validation errors as structured data — the client's `handleResponse` will surface them as an `Error`.
90
+ Return validation errors as structured data — the client's `handleResponse` will surface them as an `Error`. Check [Server Functions Guide](./server-functions.md) for more detailed examples.
89
91
 
90
92
  ## Authentication
91
93
 
92
- Use middleware before `createRPCMiddleware()`:
94
+ > Never add auth hooks inside server functions.
95
+
96
+ Use authentication middleware **before** `createRPCMiddleware()`.
93
97
 
94
98
  ```ts
95
- // Express
96
99
  app.use(authMiddleware);
97
100
  app.use(createRPCMiddleware());
98
101
  ```
99
102
 
100
- > Never add auth hooks inside the plugin.
103
+
104
+ ### Basic Authorization
105
+
106
+ A typical auth middleware reads credentials from the request, validates them, and sets `req.user`:
107
+
108
+ ```ts
109
+ // Express
110
+ const authMiddleware = (req, res, next) => {
111
+ const token = req.headers.authorization?.replace("Bearer ", "");
112
+ if (!token) {
113
+ res.status(401).json({ error: "Unauthorized" });
114
+ return;
115
+ }
116
+ try {
117
+ req.user = verifyToken(token); // { id, role, ... }
118
+ next();
119
+ } catch {
120
+ res.status(401).json({ error: "Unauthorized" });
121
+ }
122
+ };
123
+ ```
124
+
125
+ > In your production apps, **use best solutions** provided by server of your choice. The above code is only to showcase how to wire pieces together.
126
+
127
+ ### Per-Function Authorization
128
+
129
+ When some server functions should be public and others require authentication, use `createMiddleware` with a handler that inspects the function name:
130
+
131
+ ```ts
132
+ import type { Request } from "express";
133
+ import { createMiddleware } from "@thednp/rpc/express";
134
+ import { loadRPCConfig } from "@thednp/rpc";
135
+
136
+ const publicFns = new Set(["login", "register", "publicData"]);
137
+ const config = await loadRPCConfig();
138
+
139
+ // use a type to better describe your requests
140
+ type UserRequest = Request & {
141
+ user: YourUserType
142
+ }
143
+
144
+ const rpcAuthz = createMiddleware({
145
+ rpcPrefix: config.rpcPrefix,
146
+ handler: async (req: UserRequest, res, next) => {
147
+ const url = new URL(req.url, "http://localhost").pathname;
148
+ const fnName = url.replace(config.rpcPrefix, "");
149
+ const user = req.user; // set by earlier auth middleware
150
+
151
+ // Allow public functions through
152
+ if (publicFns.has(fnName)) return next();
153
+
154
+ // Reject unauthenticated
155
+ if (!user) {
156
+ res.statusCode = 401;
157
+ res.setHeader("Content-Type", "application/json");
158
+ res.end(JSON.stringify({ error: "Unauthorized" }));
159
+ return;
160
+ }
161
+
162
+ next();
163
+ },
164
+ });
165
+ ```
166
+
167
+ Wire it between auth and the RPC handler:
168
+
169
+ ```ts
170
+ app.use(authMiddleware); // sets req.user
171
+ app.use(rpcAuthz); // per-function guard
172
+ app.use(createRPCMiddleware());
173
+ ```
174
+
175
+ For role-based access, extend with a map:
176
+
177
+ ```ts
178
+ const roleAccess: Record<string, string[]> = {
179
+ deleteUser: ["admin"],
180
+ updateProfile: ["user", "admin"],
181
+ };
182
+
183
+ if (!user || !roleAccess[fnName]?.includes(user.role)) {
184
+ // 401 or 403
185
+ }
186
+ ```
101
187
 
102
188
  ## Body Limits
103
189
 
104
- In most cases you can rely on your framework's body-parser middleware:
190
+ In most cases you should rely on your framework's body-parser middleware:
105
191
 
106
192
  ```ts
107
193
  // Express
@@ -136,8 +222,9 @@ const app = new Koa();
136
222
  app.use(koaBody({ jsonLimit: 1024 * 1024 })); // 1MB
137
223
  ```
138
224
 
225
+ In other cases, your custom [server app](../examples/ssr/http-express.ts) can use something like this:
139
226
  ```ts
140
- // SSR (custom http server with Vite middleware mode)
227
+ // SSR (custom node:http server with Vite middleware mode)
141
228
  import { createMiddleware, readBody } from "@thednp/rpc/express";
142
229
  import { loadRPCConfig } from "@thednp/rpc";
143
230
 
@@ -145,7 +232,7 @@ const config = await loadRPCConfig();
145
232
  const MAX_BODY_SIZE = 1024 * 1024;
146
233
 
147
234
  app.use(createMiddleware({
148
- rpcPreffix: config.rpcPreffix,
235
+ rpcPrefix: config.rpcPrefix,
149
236
  handler: async (req, res, next) => {
150
237
  const { data } = await readBody(req);
151
238
  if (Buffer.byteLength(typeof data === "string" ? data : JSON.stringify(data)) > MAX_BODY_SIZE) {
@@ -159,6 +246,7 @@ app.use(createMiddleware({
159
246
  }));
160
247
  ```
161
248
 
249
+ For SPA you can make use of the vite runtime [proxy](../examples/spa/vite.config.ts)
162
250
  ```ts
163
251
  // SPA (dedicated RPC proxy server)
164
252
  import { readBody } from "@thednp/rpc/express";
@@ -177,10 +265,85 @@ const bodyLimit = async (req, res, next) => {
177
265
  };
178
266
  ```
179
267
 
268
+ ## Rate Limiting
269
+
270
+ Throttle RPC endpoints like any other route — rate limiting is the host's responsibility:
271
+
272
+ ```ts
273
+ // Express
274
+ import { rateLimit } from 'express-rate-limit';
275
+
276
+ app.use('/__rpc', rateLimit({
277
+ windowMs: 60 * 1000, // 1 minute
278
+ limit: 60, // 60 requests per window
279
+ standardHeaders: 'draft-8',
280
+ legacyHeaders: false,
281
+ }));
282
+ app.use(createRPCMiddleware());
283
+ ```
284
+
285
+ ```ts
286
+ // Fastify
287
+ import rateLimit from '@fastify/rate-limit';
288
+
289
+ await app.register(rateLimit, { max: 60, timeWindow: '1 minute' });
290
+ ```
291
+
292
+ ```ts
293
+ // Hono
294
+ import { rateLimiter } from 'hono-rate-limiter';
295
+
296
+ app.use('/__rpc/*', rateLimiter({ windowMs: 60_000, limit: 60 }));
297
+ ```
298
+
299
+ ```ts
300
+ // Koa
301
+ import { rateLimit } from 'koa2-ratelimit';
302
+
303
+ app.use(rateLimit.middleware({ interval: { min: 1 }, max: 60 }));
304
+ ```
305
+
306
+ ## Origin / CSRF Protection
307
+
308
+ `@thednp/rpc` performs **no origin validation by default** — like authentication, it is opt-in so the host decides.
309
+
310
+ ### Option A: `origin` middleware option
311
+
312
+ When your RPC endpoints sit behind a reverse proxy with multiple public origins (e.g. a sibling subdomain), pass the allowed origin to `createRPCMiddleware()`:
313
+
314
+ ```ts
315
+ app.use(createRPCMiddleware({
316
+ origin: 'https://app.example.com',
317
+ }));
318
+ ```
319
+
320
+ Requests carrying an `Origin` header that does not match are rejected with `403 Forbidden`. Requests without an `Origin` header (curl, native apps) pass through unchecked.
321
+
322
+ > `SameSite=Lax` cookies already block cross-site `POST` from HTML forms; the `origin` option closes the remaining "sibling subdomain" case.
323
+
324
+ ### Option B: custom middleware
325
+
326
+ Check `Sec-Fetch-Site` (Fetch Metadata) or `Origin` yourself before the RPC middleware:
327
+
328
+ ```ts
329
+ // Express
330
+ app.use((req, res, next) => {
331
+ const site = req.headers['sec-fetch-site'];
332
+ if (site && site !== 'same-origin' && site !== 'none') {
333
+ res.status(403).json({ error: 'Forbidden' });
334
+ return;
335
+ }
336
+ next();
337
+ });
338
+ app.use(createRPCMiddleware());
339
+ ```
340
+
341
+ Note that strict checks rejecting requests *without* these headers (TanStack Start style) will break curl and native clients — only enforce when the header is present, or provide an opt-out.
342
+
180
343
  ## SSR Guidance
181
344
 
182
345
  - On the **server**, `createServerFunction` runs directly (not Vite-transformed).
183
- - On the **client**, the plugin replaces server function calls with `fetch`-based client modules.
346
+ - On the **client**, the plugin replaces server function calls with `fetch` based client modules.
184
347
  - Keep `src/entry-client.ts` and `src/entry-server.ts` separate for proper hydration.
185
348
 
186
349
  ## File Naming
@@ -1,5 +1,9 @@
1
1
  # Client Usage
2
2
 
3
+ Server functions, despite their name, work in both server and client side (transformed into `fetch` based modules by our plugin), a perfect fit for isomorphic rendering.
4
+
5
+ In most apps you will be working with client focused apps.
6
+
3
7
  ## Auto-Generated Client Modules
4
8
 
5
9
  When you import from `./api` in your client code, the plugin intercepts the import and generates a client module for each server function.
@@ -15,7 +19,7 @@ Each imported function returns:
15
19
  ```
16
20
 
17
21
  - **`data`** — A promise that resolves to the server function's return value.
18
- - **`cancel(reason)`** — Aborts the underlying fetch request, causing `signal.aborted` to be set in the server function.
22
+ - **`cancel(reason: string)`** — Aborts the underlying fetch request, causing `signal.aborted` to be set in the server function.
19
23
 
20
24
  ### Example
21
25
 
@@ -31,11 +35,11 @@ cancel('user cancelled'); // triggers AbortController on the client side
31
35
 
32
36
  - **Fetch errors** (network failure, CORS) — thrown from `await data`
33
37
  - **HTTP 4xx/5xx responses** — thrown from `await data`
34
- - **Cancellation** — returns `"Request cancelled by user"` from `await data`
38
+ - **Cancellation** — aborts the fetch and warns `"Request was cancelled"` in the console
35
39
 
36
40
  ## @tanstack/react-query Integration
37
41
 
38
- `@thednp/rpc` is a dumb pipe — it handles serialization and transport only. For client-side caching, invalidation, and stale-while-revalidate patterns, use `@tanstack/react-query`:
42
+ `@thednp/rpc` is a transport pipe — it handles serialization and transport only. For client-side caching, data invalidation, and stale-while-revalidate patterns, use `@tanstack/react-query`:
39
43
 
40
44
  ```ts
41
45
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
@@ -8,7 +8,7 @@ Create `rpc.config.ts` in your project root for system-wide configuration:
8
8
  import { defineConfig } from '@thednp/rpc';
9
9
 
10
10
  export default defineConfig({
11
- rpcPreffix: '__rpc',
11
+ rpcPrefix: '__rpc',
12
12
  adapter: 'express',
13
13
  });
14
14
  ```
@@ -17,7 +17,6 @@ export default defineConfig({
17
17
 
18
18
  Update your `vite.config.ts` in your project root and set additional development options:
19
19
 
20
-
21
20
  ```ts
22
21
  import { defineConfig } from 'vite';
23
22
  import rpc from '@thednp/rpc';
@@ -34,12 +33,12 @@ export default defineConfig({
34
33
 
35
34
  | Option | Type | Default | Description |
36
35
  | --------------| ----------| -------------| --------------------------------------------------------------|
37
- | `rpcPreffix` | `string` | `'__rpc'` | RPC endpoint prefix used in URL routing |
36
+ | `rpcPrefix` | `string` | `'__rpc'` | RPC endpoint prefix used in URL routing |
38
37
  | `adapter` | `string` | `'express'` | Target adapter (`'express'`, `'fastify'`, `'hono'`, `'koa'`) |
39
38
 
40
39
  ## Config File Discovery
41
40
 
42
- When `configFile` is not specified, the plugin searches for config files in this order:
41
+ The plugin searches for config files in this order:
43
42
 
44
43
  1. `rpc.config.ts`
45
44
  2. `rpc.config.js`
@@ -60,7 +59,7 @@ Type-safe helper for creating the config object. Provides autocomplete and type
60
59
  import { defineConfig } from '@thednp/rpc';
61
60
 
62
61
  export default defineConfig({
63
- rpcPreffix: '__rpc',
62
+ rpcPrefix: '__rpc',
64
63
  });
65
64
  ```
66
65
 
@@ -72,6 +71,6 @@ Programmatically load the RPC config, useful in custom server setups:
72
71
  import { loadRPCConfig } from '@thednp/rpc';
73
72
 
74
73
  const config = await loadRPCConfig();
75
- console.log(config.rpcPreffix); // '__rpc'
74
+ console.log(config.rpcPrefix); // '__rpc'
76
75
  console.log(config.adapter); // 'express'
77
76
  ```
@@ -3,35 +3,52 @@
3
3
  ## Installation
4
4
 
5
5
  ```bash
6
- pnpm add @thednp/rpc@latest
6
+ // pnpm + jsr registry
7
+ pnpm add jsr:@thednp/rpc
7
8
  ```
8
9
 
9
10
  ```bash
10
- npm install @thednp/rpc@latest
11
+ // pnpm + npmjs registry
12
+ pnpm add @thednp/rpc
11
13
  ```
12
14
 
13
15
  ```bash
14
- bun add @thednp/rpc@latest
16
+ // npm + npmjs registry
17
+ npm install @thednp/rpc
15
18
  ```
16
19
 
17
20
  ```bash
18
- deno add npm:@thednp/rpc@latest
21
+ // bun + npmjs registry
22
+ bun add @thednp/rpc
23
+ ```
24
+
25
+ ```bash
26
+ // deno + npmjs registry
27
+ deno add npm:@thednp/rpc
28
+ ```
29
+
30
+ ```bash
31
+ // deno + jsr registry
32
+ deno add jsr:@thednp/rpc
19
33
  ```
20
34
 
21
35
  ## Quick Start
22
36
 
37
+ For a quick understanding of a project setup check the [dedicated wiki section](./setup.md).
38
+
23
39
  ### 1. Configure system wide configuration `rpc.config.ts`
24
40
 
25
41
  ```ts
26
42
  import { defineConfig } from "@thednp/rpc";
27
43
 
28
44
  export default defineConfig({
29
- rpcPreffix: "__server",
45
+ rpcPrefix: "__rpc",
30
46
  adapter: "express",
31
47
  });
32
48
  ```
33
49
 
34
50
  Currently `@thednp/rpc` supports `'express'`, `'fastify'`, `'hono'` and `'koa'`. Check [adapters](./adapters.md) for more guides.
51
+
35
52
  Also check [configuration](./configuration.md) for more guides.
36
53
 
37
54
 
@@ -63,7 +80,15 @@ export const sayHi = createServerFunction(
63
80
  );
64
81
  ```
65
82
 
66
- ### 4. Use it on the client
83
+ Expose it in `src/api/index.ts`
84
+
85
+ ```ts
86
+ export * from "./server"
87
+ ```
88
+
89
+ Check the [server functions guide](./server-functions.md) for details.
90
+
91
+ ### 4. Use it in your code
67
92
 
68
93
  ```ts
69
94
  import { sayHi } from './api';
@@ -74,3 +99,7 @@ cancel('user cancelled');
74
99
  ```
75
100
 
76
101
  That's it — the plugin auto-scans `src/api/server.ts`, maps exports to client functions, and replaces `./api` imports with fetch-based client modules during the Vite transform.
102
+
103
+ For a more detailed guide on client-side usage, check the [dedicated wiki section](./client-usage.md).
104
+
105
+ Next you need to [connect the adapter](./adapters.md) with your server of choice.
package/wiki/index.md CHANGED
@@ -2,25 +2,13 @@
2
2
 
3
3
  A Vite plugin for creating server functions with automatic Remote Procedure Calls (RPC) generation. Server functions are defined in a dedicated file, auto-scanned, and transformed into client-side fetch modules — no manual API route setup required.
4
4
 
5
- ## Features
6
-
7
- - File-level server code isolation without directives like `'use server'`
8
- - System-wide configuration via `rpc.config.ts`
9
- - Automatic RPC generation for server functions
10
- - `AbortController` support for request cancellation
11
- - Adapters for Express, Fastify, Hono, and Koa with unified API
12
- - Framework-agnostic core
13
- - TypeScript support
14
- - Path-segment prefix matching prevents URL boundary bypass
15
- - @tanstack/react-query integration for client-side caching and invalidation
16
-
17
5
  ## Table of Contents
18
6
 
19
- - [Getting Started](getting-started.md) — Installation and quick start
20
- - [Setup Guide](setup.md) — Project structure and configuration
21
- - [Configuration](configuration.md) — Configuration reference
22
- - [Server Functions](server-functions.md) — Creating server functions
23
- - [Client Usage](client-usage.md) — Client-side usage
24
- - [Adapters](adapters.md) — Framework adapters
25
- - [Security](security.md) — Security hardening
26
- - [Best Practices](best-practices.md) — Tips and best practices
7
+ - [Getting Started](./getting-started.md) — Installation and quick start
8
+ - [Setup Guide](./setup.md) — Project structure and configuration
9
+ - [Configuration](./configuration.md) — Configuration reference
10
+ - [Server Functions](./server-functions.md) — Creating server functions
11
+ - [Client Usage](./client-usage.md) — Client-side usage
12
+ - [Adapters](./adapters.md) — Framework adapters
13
+ - [Security](./security.md) — Security hardening
14
+ - [Best Practices](./best-practices.md) — Tips and best practices
package/wiki/security.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  ## Prefix Boundary Check
4
4
 
5
- All adapters use `new RegExp(\`^/${rpcPreffix}/\`)` instead of `startsWith` to match the RPC endpoint path. This prevents path-segment bypass attacks:
5
+ All adapters use `new RegExp(\`^/${escapeRegExp(rpcPrefix)}/\`)` instead of `startsWith` to match the RPC endpoint path. The prefix is escaped with `escapeRegExp()` before being embedded in the boundary regex, preventing ReDoS or unintended matching from metacharacters in the prefix. This prevents path-segment bypass attacks:
6
6
 
7
7
  ```
8
- rpcPreffix = '__rpc'
8
+ rpcPrefix = '__rpc'
9
9
 
10
10
  # Safe: matches /__rpc/foo
11
11
  // __rpc/foo → /__rpc/
@@ -28,6 +28,27 @@ const pathname = url.pathname; // clean, no query string
28
28
 
29
29
  Error responses do not echo the requested function name. This prevents function enumeration — an attacker cannot discover available RPC functions by probing for non-existent endpoints.
30
30
 
31
+ ## HTTP Method Enforcement
32
+
33
+ Server functions default to `POST`, and the middleware rejects any request whose HTTP method does not match the function's configured method with `405 Method Not Allowed`:
34
+
35
+ ```
36
+ GET /__rpc/do-stuff → 405 (function defaults to POST)
37
+ POST /__rpc/do-stuff → 200
38
+ ```
39
+
40
+ This blocks the simplest CSRF vector: an attacker page embedding `<img src="/__rpc/do-stuff">` or a form `GET` that would otherwise trigger side effects. Functions that opt into `method: "GET"` (via `createServerFunction(name, handler, { method: 'GET' })`) receive their arguments as an `?args=` JSON query parameter. Reserve `GET` for side-effect-free functions only. See [Server Functions Guide](./server-functions.md) for details.
41
+
42
+ ## Origin Validation
43
+
44
+ `@thednp/rpc` performs no origin validation by default, but `createRPCMiddleware()` accepts an `origin` option:
45
+
46
+ ```ts
47
+ app.use(createRPCMiddleware({ origin: 'https://app.example.com' }));
48
+ ```
49
+
50
+ When set, any request carrying an `Origin` header that does not match the configured origin is rejected with `403 Forbidden`. Requests **without** an `Origin` header (curl, native clients) pass through — the check only rejects when the browser-provided header disagrees. This closes the "sibling subdomain" CSRF gap that `SameSite=Lax` cookies alone cannot cover. See [Best Practices Guide](./best-practices.md) for custom middleware alternatives.
51
+
31
52
  ## Authentication via Middleware
32
53
 
33
54
  Authentication is handled by middleware registered **before** `createRPCMiddleware()`. The middleware chain composes naturally:
@@ -38,7 +59,7 @@ app.use(authMiddleware); // auth first
38
59
  app.use(createRPCMiddleware()); // RPC second
39
60
  ```
40
61
 
41
- Do not add auth hooks inside the plugin. Use your framework's standard middleware pattern.
62
+ Do not add auth hooks inside the plugin. Use your framework's standard middleware pattern. Check [Best Practices Guide](./best-practices.md) for more detailed examples.
42
63
 
43
64
  ## Body Size Limits
44
65
 
@@ -49,6 +70,7 @@ JSON body size limits are handled by your framework's body-parser middleware:
49
70
  - **Koa**: `koa-body({ formLimit: '1mb' })`
50
71
  - **Hono**: Built-in body size limiting
51
72
 
52
- Since the RPC framework's `readBody` also accepts `text/plain` requests (not parsed by the JSON body parser), the raw stream path in the Express and Koa adapters has a built-in safety net — a `maxBodySize` parameter that defaults to **1 MiB**. Pass a custom value to `readBody(req, signal, myLimit)` to override.
73
+ The `readBody` utility of each adapter reads the raw request stream and does **not** impose a built-in size limit — always register your framework's body-parser middleware (or a custom limit handler) before `createRPCMiddleware()`. Check [Best Practices Guide](./best-practices.md) for body-limit examples.
53
74
 
54
- Register body parsing middleware before `createRPCMiddleware()`.
75
+ ## Input Validation
76
+ Server functions receive raw, untrusted client data. Always validate before use. Check [Server Functions Guide](./server-functions.md) for more detailed examples.
@@ -1,5 +1,15 @@
1
1
  # Server Functions
2
2
 
3
+ ## Overview
4
+
5
+ Server functions run exclusively on the server. They have access to server-only resources (databases, file system, environment variables, private APIs) and **never execute on the client**.
6
+
7
+ The `@thednp/rpc` Vite plugin transforms imports of server functions into client-side stubs that call the real implementation over HTTP. This isomorphic bridge means you write your functions once and call them from either server-rendered pages or client-side code — the RPC middleware handles routing on the server while the generated client modules handle serialization, transport, and cancellation.
8
+
9
+ All examples except the SPA use SSR to demonstrate this: the same server functions are imported directly during server-side rendering (in `entry-server.ts`) and also called from client-side JavaScript (via the auto-generated fetch module).
10
+
11
+ The [SPA example](../examples/spa) uses a thin `node:http` based proxy that executes the server functions.
12
+
3
13
  ## `createServerFunction(name, handler, options?)`
4
14
 
5
15
  The core API for defining server-side functions.
@@ -10,15 +20,49 @@ The core API for defining server-side functions.
10
20
  function createServerFunction<T>(
11
21
  name: string,
12
22
  handler: (signal: AbortSignal, ...args: JsonArray) => Promise<T>,
13
- options?: { contentType: 'application/json' | 'text/plain'}
23
+ options?: {
24
+ contentType?: 'application/json' | 'text/plain',
25
+ credentials?: "same-origin" | "include" | "omit",
26
+ method?: "GET" | "POST",
27
+ }
14
28
  ): ServerFunction<T>;
15
29
  ```
16
30
 
17
31
  ### Parameters
18
32
 
19
- - **`name`** (`string`) — The registered name used in RPC routing. Must match the name used when calling the function on the client.
33
+ - **`name`** (`string`) — The registered name used in RPC routing.
20
34
  - **`handler`** (`(signal: AbortSignal, ...args: JsonArray) => Promise<T>`) — The actual implementation. The first argument is always an `AbortSignal`; remaining arguments come from the client. The return value must be JSON-serializable.
21
- - **`options`** (`{ contentType?: 'application/json' | 'text/plain' }`) — Optional serialization strategy. Defaults to `'application/json'`.
35
+ - **`options`** — Optional credentials, serialization strategy, and HTTP method
36
+ * `contentType?: 'application/json' | 'text/plain'` - Defaults to `'application/json'`.
37
+ * `credentials?: "include" | "same-origin" | "omit"` - Defaults to `'same-origin'`.
38
+ * `method?: "GET" | "POST"` - Defaults to `'POST'`.
39
+
40
+ ### HTTP Method
41
+
42
+ By default every server function is invoked via `POST`. Functions that are safe to call from a browser URL bar, a `<script>` tag, or a CDN can opt into `GET` — arguments then travel as an `?args=` JSON query parameter:
43
+
44
+ ```ts
45
+ export const publicData = createServerFunction(
46
+ 'public-data',
47
+ async (signal, topic: string) => {
48
+ return await fetchPublicData(topic);
49
+ },
50
+ { method: 'GET' },
51
+ );
52
+ ```
53
+
54
+ The generated client module issues a `GET /__rpc/public-data?args=%5B%22news%22%5D` request. The middleware rejects requests whose HTTP method does not match the function's configured method with `405 Method Not Allowed` — so `POST`-only functions are safe from cross-site `GET` requests, and `GET` functions can be linked/bookmarked directly.
55
+
56
+ > **Security note:** defaulting to `POST` prevents CSRF via `<img>`/`<script>`/form `GET` requests. Only set `method: "GET"` for functions with no side effects.
57
+
58
+ > **Why only `GET` and `POST`?** This is deliberate, not an oversight:
59
+ >
60
+ > - RPC dispatch is not REST — functions have no resource semantics, so the meanings of `PUT` (idempotent replace), `PATCH` (partial update), or `DELETE` (removal) don't apply to a function call. The only transport distinctions that matter are `POST` (args in the body, any payload) and `GET` (args in the query string, cacheable by browsers and CDNs).
61
+ > - `OPTIONS` is reserved by the HTTP protocol for CORS preflight; browsers send it automatically, and frameworks handle it. Exposing it as a function method would collide with framework CORS handling.
62
+ > - `HEAD` is derived from `GET` at the HTTP layer, so it needs no function-level support.
63
+ > - Every accepted method is another dispatch path to validate. Keeping the surface minimal (and defaulting to `POST`) reduces CSRF and parsing attack surface.
64
+ >
65
+ > If a concrete need arises (e.g. a REST-style wrapper wanting true `PUT` semantics), the `method` union is a one-line extension — adapters already centralize dispatch on it.
22
66
 
23
67
  ### AbortSignal
24
68
 
@@ -44,11 +88,11 @@ When `createServerFunction` is called, it registers the function in a server-sid
44
88
 
45
89
  ### Return Type
46
90
 
47
- The return value of `handler` is serialized to JSON and sent as the HTTP response body. Ensure your return type is JSON-serializable.
91
+ The return value of `handler` is serialized to JSON and sent as the HTTP response body. **Ensure your return type is JSON-serializable.**
48
92
 
49
93
  ## Input Validation
50
94
 
51
- Server functions receive raw, untrusted client data. Always validate before use.
95
+ Server functions receive raw, untrusted client data. **Always validate data within your server functions before use.**
52
96
 
53
97
  **zod:**
54
98
 
package/wiki/setup.md CHANGED
@@ -6,17 +6,21 @@
6
6
  project/
7
7
  ├── src/
8
8
  │ ├── api/
9
+ │ │ └── index.ts # All server functions exports
9
10
  │ │ └── server.ts # Auto-scanned server functions
10
11
  │ ├── entry-client.ts # Client entry (SSR projects)
11
12
  │ └── entry-server.ts # Server entry (SSR projects)
12
13
  ├── vite.config.ts # Add rpc() plugin here
13
14
  ├── rpc.config.ts # Optional config
14
- └── package.json
15
+ ├── package.json # The project npm configuration
16
+ └── server.js # Your Express/Hono/Fastify/Koa server
15
17
  ```
16
18
 
19
+ > Various frameworks like `@tanstack-start`, `@sveltejs/kit` prefer a more specific structure, so be sure to check their documentation; most frameworks have their own data transport layer.
20
+
17
21
  ## Server Files
18
22
 
19
- The plugin looks in `src/api/` for files matching these names:
23
+ The plugin looks in `src/api/` for files with these **exact** names (matching is case-sensitive and non-partial, so `server.tsx`, `my-server.ts`, or `server.txt` are ignored):
20
24
 
21
25
  - `server.ts`
22
26
  - `server.js`
@@ -36,7 +40,7 @@ Each matched file is loaded with `vite.ssrLoadModule`, and all named exports are
36
40
 
37
41
  ### SSR Projects
38
42
 
39
- Create both `src/entry-client.ts` and `src/entry-server.ts`. The client bundle imports from `./api`, which gets transformed by the plugin. On the server, `createServerFunction` runs directly (not transformed).
43
+ Create both `src/entry-client.ts` and `src/entry-server.ts`. The client and server bundles both import from `./api`. On the client your server function gets transformed by the plugin. On the server, `createServerFunction` runs directly (not transformed).
40
44
 
41
45
  ### SPA Projects
42
46
 
@@ -74,4 +78,4 @@ await attachRPC(app); // production
74
78
  attachVite(app, vite); // development
75
79
  ```
76
80
 
77
- See [Adapters](adapters.md) for full server setup examples.
81
+ See [Adapters](./adapters.md) for full server setup examples.