@thednp/rpc 0.0.4 → 0.0.6

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.
@@ -1,364 +0,0 @@
1
- # Best Practices
2
-
3
- ## @thednp/rpc is a Transport Pipe
4
-
5
- The best thing to do **first** is to understand `@thednp/rpc` is that it only handles **transport** and **serialization**.
6
-
7
- It does not provide:
8
- - Caching
9
- - Authentication
10
- - Validation
11
- - State management
12
- - Request limits
13
- - Retry logic
14
-
15
- Use dedicated tools for these concerns.
16
-
17
- ## Client-Side Caching
18
-
19
- All major frameworks have a `@tanstack/<framework>-query` made by [Tanstack](https://tanstack.com/) to cover all needs except validation and authentication.
20
-
21
- For instance the `@tanstack/react-query` is the recommended layer for React apps client-side caching, invalidation, and stale-while-revalidate:
22
-
23
- ```ts
24
- // src/components/GreetUser.tsx
25
- import { useQuery, useQueryClient } from '@tanstack/react-query';
26
- import { sayHi } from '../api';
27
-
28
- function GreetUser({ name }: { name: string }) {
29
- const { data } = useQuery({
30
- queryKey: ['say-hi', name],
31
- queryFn: ({ signal }) => {
32
- const result = sayHi(name);
33
- signal.addEventListener('abort', () => result.cancel('query cancelled'));
34
- return result.data;
35
- },
36
- });
37
- return <div>{data ?? 'Loading...'}</div>;
38
- }
39
- ```
40
-
41
- ## AbortSignal Best Practices
42
-
43
- Always check `signal.aborted` or call `signal.throwIfAborted()` in long-running server functions:
44
-
45
- ```ts
46
- export const processBatch = createServerFunction(
47
- 'process-batch',
48
- async (signal: AbortSignal, items: string[]) => {
49
- const results = [];
50
- const errors = [];
51
- for (const item of items) {
52
- signal.throwIfAborted();
53
- const result = await heavyWork(item);
54
- results.push(result);
55
- // handle errors properly
56
- }
57
- return results;
58
- },
59
- );
60
- ```
61
-
62
- ## Input Validation
63
-
64
- Always validate client-provided data before using it in server functions. Use libraries like **zod** or **valibot** to parse and validate inputs:
65
-
66
- ```ts
67
- // src/api/server.ts
68
- import { z } from 'zod';
69
- import { createServerFunction } from '@thednp/rpc/server';
70
-
71
- const ProfileSchema = z.object({
72
- name: z.string().min(1).max(100),
73
- age: z.number().int().positive(),
74
- });
75
-
76
- export const updateProfile = createServerFunction(
77
- 'update-profile',
78
- async (signal, raw) => {
79
- const parsed = ProfileSchema.safeParse(raw);
80
- if (!parsed.success) {
81
- return { ok: false, errors: parsed.error.flatten().fieldErrors };
82
- }
83
- // parsed.data is fully typed
84
- await saveToDb(parsed.data);
85
- return { ok: true };
86
- },
87
- );
88
- ```
89
-
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.
91
-
92
- ## Authentication
93
-
94
- > Never add auth hooks inside server functions.
95
-
96
- Use authentication middleware **before** `createRPCMiddleware()`.
97
-
98
- ```ts
99
- app.use(authMiddleware);
100
- app.use(createRPCMiddleware());
101
- ```
102
-
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
- ```
187
-
188
- ## Body Limits
189
-
190
- In most cases you should rely on your framework's body-parser middleware:
191
-
192
- ```ts
193
- // Express
194
- import express from "express";
195
- const app = express();
196
- app.use(express.json({ limit: 1024 * 1024 })); // or "1mb"
197
- app.use(createRPCMiddleware());
198
- ```
199
-
200
- ```ts
201
- // Fastify
202
- import Fastify from "fastify";
203
-
204
- const app = Fastify({ logger: false, bodyLimit: 1024 * 1024 }); // "1MB"
205
- ```
206
-
207
- ```ts
208
- // Hono
209
- import { Hono } from "hono";
210
- import { bodyLimit } from 'hono/body-limit';
211
-
212
- const app = new Hono();
213
- app.use('*', bodyLimit({ maxSize: 1024 * 1024 })) // 1MB
214
- ```
215
-
216
- ```ts
217
- // Koa
218
- import Koa from "koa";
219
- import { koaBody } from 'koa-body';
220
-
221
- const app = new Koa();
222
- app.use(koaBody({ jsonLimit: 1024 * 1024 })); // 1MB
223
- ```
224
-
225
- In other cases, your custom [server app](../examples/ssr/http-express.ts) can use something like this:
226
- ```ts
227
- // SSR (custom node:http server with Vite middleware mode)
228
- import { createMiddleware, readBody } from "@thednp/rpc/express";
229
- import { loadRPCConfig } from "@thednp/rpc";
230
-
231
- const config = await loadRPCConfig();
232
- const MAX_BODY_SIZE = 1024 * 1024;
233
-
234
- app.use(createMiddleware({
235
- rpcPrefix: config.rpcPrefix,
236
- handler: async (req, res, next) => {
237
- const { data } = await readBody(req);
238
- if (Buffer.byteLength(typeof data === "string" ? data : JSON.stringify(data)) > MAX_BODY_SIZE) {
239
- res.statusCode = 413;
240
- res.end("Payload Too Large");
241
- return;
242
- }
243
- req.body = data;
244
- next();
245
- },
246
- }));
247
- ```
248
-
249
- For SPA you can make use of the vite runtime [proxy](../examples/spa/vite.config.ts)
250
- ```ts
251
- // SPA (dedicated RPC proxy server)
252
- import { readBody } from "@thednp/rpc/express";
253
-
254
- const MAX_BODY_SIZE = 1024 * 1024;
255
-
256
- const bodyLimit = async (req, res, next) => {
257
- const { data } = await readBody(req);
258
- if (Buffer.byteLength(typeof data === "string" ? data : JSON.stringify(data)) > MAX_BODY_SIZE) {
259
- res.statusCode = 413;
260
- res.end("Payload Too Large");
261
- return;
262
- }
263
- req.body = data;
264
- next();
265
- };
266
- ```
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
-
343
- ## SSR Guidance
344
-
345
- - On the **server**, `createServerFunction` runs directly (not Vite-transformed).
346
- - On the **client**, the plugin replaces server function calls with `fetch` based client modules.
347
- - Keep `src/entry-client.ts` and `src/entry-server.ts` separate for proper hydration.
348
-
349
- ## File Naming
350
-
351
- Only files matching `server.ts`, `server.js`, `server.mjs`, `server.mts` in `src/api/` are scanned. Export functions individually for proper client module mapping:
352
-
353
- ```ts
354
- // ✅ Good — individual exports
355
- export const sayHi = createServerFunction('say-hi', fn);
356
- export const add = createServerFunction('add-numbers', fn);
357
-
358
- // ❌ Avoid — default export or bundled objects
359
- export default { sayHi, add };
360
- ```
361
-
362
- ## Cache is Not @thednp/rpc's Job
363
-
364
- Use `react-query`, `SWR`, or your framework's data hooks for caching. `@thednp/rpc` is the **transport layer only**.
@@ -1,66 +0,0 @@
1
- # Client Usage
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
-
7
- ## Auto-Generated Client Modules
8
-
9
- When you import from `./api` in your client code, the plugin intercepts the import and generates a client module for each server function.
10
-
11
- ```ts
12
- import { sayHi, add } from './api';
13
- ```
14
-
15
- Each imported function returns:
16
-
17
- ```ts
18
- { data: Promise<T>, cancel: (reason: string) => void }
19
- ```
20
-
21
- - **`data`** — A promise that resolves to the server function's return value.
22
- - **`cancel(reason: string)`** — Aborts the underlying fetch request, causing `signal.aborted` to be set in the server function.
23
-
24
- ### Example
25
-
26
- ```ts
27
- import { sayHi } from './api';
28
-
29
- const { data, cancel } = sayHi('World');
30
- const result = await data; // "Hello World!"
31
- cancel('user cancelled'); // triggers AbortController on the client side
32
- ```
33
-
34
- ## Error Handling
35
-
36
- - **Fetch errors** (network failure, CORS) — thrown from `await data`
37
- - **HTTP 4xx/5xx responses** — thrown from `await data`
38
- - **Cancellation** — aborts the fetch and warns `"Request was cancelled"` in the console
39
-
40
- ## @tanstack/react-query Integration
41
-
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`:
43
-
44
- ```ts
45
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
46
- import { sayHi } from './api';
47
-
48
- function GreetUser({ name }: { name: string }) {
49
- const queryClient = useQueryClient();
50
-
51
- const { data } = useQuery({
52
- queryKey: ['say-hi', name],
53
- queryFn: ({ signal }) => {
54
- const result = sayHi(name);
55
- signal.addEventListener('abort', () => result.cancel('query cancelled'));
56
- return result.data;
57
- },
58
- });
59
-
60
- return <div>{data ?? 'Loading...'}</div>;
61
- }
62
- ```
63
-
64
- Combine `cancel()` with React Query's `signal` for proper abort handling during component unmount or query invalidation.
65
-
66
- Other frameworks have a `@tanstack/<framework>-query` made by [Tanstack](https://tanstack.com/).
@@ -1,76 +0,0 @@
1
- # Configuration
2
-
3
- ## `rpc.config.ts`
4
-
5
- Create `rpc.config.ts` in your project root for system-wide configuration:
6
-
7
- ```ts
8
- import { defineConfig } from '@thednp/rpc';
9
-
10
- export default defineConfig({
11
- rpcPrefix: '__rpc',
12
- adapter: 'express',
13
- });
14
- ```
15
-
16
- ## `vite.config.ts`
17
-
18
- Update your `vite.config.ts` in your project root and set additional development options:
19
-
20
- ```ts
21
- import { defineConfig } from 'vite';
22
- import rpc from '@thednp/rpc';
23
-
24
- export default defineConfig({
25
- plugins: [rpc(/* development options */)]
26
- });
27
-
28
- ```
29
-
30
- > **NOTE** these plugin options only apply to **development** and override the options in `rpc.config.ts`.
31
-
32
- ### Options
33
-
34
- | Option | Type | Default | Description |
35
- | --------------| ----------| -------------| --------------------------------------------------------------|
36
- | `rpcPrefix` | `string` | `'__rpc'` | RPC endpoint prefix used in URL routing |
37
- | `adapter` | `string` | `'express'` | Target adapter (`'express'`, `'fastify'`, `'hono'`, `'koa'`) |
38
-
39
- ## Config File Discovery
40
-
41
- The plugin searches for config files in this order:
42
-
43
- 1. `rpc.config.ts`
44
- 2. `rpc.config.js`
45
- 3. `rpc.config.mjs`
46
- 4. `rpc.config.mts`
47
- 5. `.rpcrc.ts`
48
- 6. `.rpcrc.js`
49
-
50
- The first file found is used. If none is found, defaults are applied.
51
-
52
- ## Utilities
53
-
54
- ### `defineConfig`
55
-
56
- Type-safe helper for creating the config object. Provides autocomplete and type checking for all options.
57
-
58
- ```ts
59
- import { defineConfig } from '@thednp/rpc';
60
-
61
- export default defineConfig({
62
- rpcPrefix: '__rpc',
63
- });
64
- ```
65
-
66
- ### `loadRPCConfig`
67
-
68
- Programmatically load the RPC config, useful in custom server setups:
69
-
70
- ```ts
71
- import { loadRPCConfig } from '@thednp/rpc';
72
-
73
- const config = await loadRPCConfig();
74
- console.log(config.rpcPrefix); // '__rpc'
75
- console.log(config.adapter); // 'express'
76
- ```
@@ -1,105 +0,0 @@
1
- # Getting Started
2
-
3
- ## Installation
4
-
5
- ```bash
6
- // pnpm + jsr registry
7
- pnpm add jsr:@thednp/rpc
8
- ```
9
-
10
- ```bash
11
- // pnpm + npmjs registry
12
- pnpm add @thednp/rpc
13
- ```
14
-
15
- ```bash
16
- // npm + npmjs registry
17
- npm install @thednp/rpc
18
- ```
19
-
20
- ```bash
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
33
- ```
34
-
35
- ## Quick Start
36
-
37
- For a quick understanding of a project setup check the [dedicated wiki section](./setup.md).
38
-
39
- ### 1. Configure system wide configuration `rpc.config.ts`
40
-
41
- ```ts
42
- import { defineConfig } from "@thednp/rpc";
43
-
44
- export default defineConfig({
45
- rpcPrefix: "__rpc",
46
- adapter: "express",
47
- });
48
- ```
49
-
50
- Currently `@thednp/rpc` supports `'express'`, `'fastify'`, `'hono'` and `'koa'`. Check [adapters](./adapters.md) for more guides.
51
-
52
- Also check [configuration](./configuration.md) for more guides.
53
-
54
-
55
- ### 2. Add the plugin to `vite.config.ts`
56
-
57
- ```ts
58
- import rpc from '@thednp/rpc';
59
-
60
- export default {
61
- plugins: [rpc()],
62
- };
63
- ```
64
-
65
- Check [configuration](./configuration.md) for more guides.
66
-
67
-
68
- ### 3. Create a server function in `src/api/server.ts`
69
-
70
- ```ts
71
- import { createServerFunction } from '@thednp/rpc/server';
72
-
73
- export const sayHi = createServerFunction(
74
- 'say-hi',
75
- async (signal: AbortSignal, name: string) => {
76
- signal.throwIfAborted();
77
- await new Promise((res) => setTimeout(res, 1500));
78
- return `Hello ${name}!`;
79
- },
80
- );
81
- ```
82
-
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
92
-
93
- ```ts
94
- import { sayHi } from './api';
95
-
96
- const { data, cancel } = sayHi('World');
97
- const result = await data; // "Hello World!"
98
- cancel('user cancelled');
99
- ```
100
-
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 DELETED
@@ -1,14 +0,0 @@
1
- # @thednp/rpc
2
-
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
-
5
- ## Table of Contents
6
-
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 DELETED
@@ -1,76 +0,0 @@
1
- # Security
2
-
3
- ## Prefix Boundary Check
4
-
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
-
7
- ```
8
- rpcPrefix = '__rpc'
9
-
10
- # Safe: matches /__rpc/foo
11
- // __rpc/foo → /__rpc/
12
-
13
- # Not matched: /__rpc-evil/foo or /foo/__rpc/bar
14
- ```
15
-
16
- Using `startsWith` would incorrectly match paths like `/__rpc-evil/foo`, which could route to unintended handlers. The regex ensures the prefix is a standalone path segment.
17
-
18
- ## Koa URL Normalization
19
-
20
- The Koa adapter parses `ctx.url` through `new URL()` before prefix checking. This strips query strings and normalizes encoding, preventing query-string injection attacks:
21
-
22
- ```ts
23
- const url = new URL(ctx.url, 'http://localhost');
24
- const pathname = url.pathname; // clean, no query string
25
- ```
26
-
27
- ## Generic 404 Responses
28
-
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
-
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
-
52
- ## Authentication via Middleware
53
-
54
- Authentication is handled by middleware registered **before** `createRPCMiddleware()`. The middleware chain composes naturally:
55
-
56
- ```ts
57
- // Express example
58
- app.use(authMiddleware); // auth first
59
- app.use(createRPCMiddleware()); // RPC second
60
- ```
61
-
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.
63
-
64
- ## Body Size Limits
65
-
66
- JSON body size limits are handled by your framework's body-parser middleware:
67
-
68
- - **Express**: `express.json({ limit: '1mb' })` (default **100kb**)
69
- - **Fastify**: `bodyLimit` option in Fastify config (default **1 MiB**)
70
- - **Koa**: `koa-body({ formLimit: '1mb' })`
71
- - **Hono**: Built-in body size limiting
72
-
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.
74
-
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.