@thednp/rpc 0.0.1 → 0.0.5

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 +195 -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 +83 -5
  9. package/dist/fastify/fastify.d.mts.map +1 -1
  10. package/dist/fastify/fastify.mjs +84 -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 +73 -18
  15. package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
  16. package/dist/helpers/helpers.d.mts +66 -4
  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 +75 -6
  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 +210 -19
  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 +111 -10
  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 -31
  37. package/wiki/adapters.md +0 -143
  38. package/wiki/best-practices.md +0 -201
  39. package/wiki/client-usage.md +0 -62
  40. package/wiki/configuration.md +0 -77
  41. package/wiki/getting-started.md +0 -76
  42. package/wiki/index.md +0 -26
  43. package/wiki/security.md +0 -54
  44. package/wiki/server-functions.md +0 -93
  45. package/wiki/setup.md +0 -77
package/wiki/adapters.md DELETED
@@ -1,143 +0,0 @@
1
- # Adapters
2
-
3
- `@thednp/rpc` provides adapters for ExpressJS, Fastify, Hono, and Koa. Each adapter exports `attachRPC` for production and `attachVite` for development.
4
-
5
- ## Common Pattern
6
-
7
- All adapters share the same two-function API:
8
-
9
- ```ts
10
- import { attachRPC, attachVite } from '@thednp/rpc/<adapter>';
11
-
12
- // Production: mount RPC middleware
13
- await attachRPC(app);
14
-
15
- // Development: mount RPC + Vite middleware
16
- attachVite(app, vite);
17
- ```
18
-
19
- ## Express
20
-
21
- ### Installation
22
-
23
- ```bash
24
- pnpm add @thednp/rpc express
25
- ```
26
-
27
- ### Usage
28
-
29
- ```ts
30
- // server.ts
31
- import express from 'express';
32
- import { attachRPC, attachVite } from '@thednp/rpc/express';
33
- import { createServer } from 'vite';
34
-
35
- const app = express();
36
- const isDev = process.env.NODE_ENV !== 'production';
37
-
38
- if (isDev) {
39
- const vite = await createServer({ server: { middlewareMode: true } });
40
- attachVite(app, vite);
41
- } else {
42
- await attachRPC(app);
43
- app.use(express.static('dist'));
44
- }
45
-
46
- app.listen(3000);
47
- ```
48
-
49
- ## Fastify
50
-
51
- ### Installation
52
-
53
- ```bash
54
- pnpm add @thednp/rpc fastify
55
- ```
56
-
57
- ### Usage
58
-
59
- ```ts
60
- // server.ts
61
- import Fastify from 'fastify';
62
- import { attachRPC, attachVite } from '@thednp/rpc/fastify';
63
- import { createServer } from 'vite';
64
-
65
- const fastify = Fastify({ logger: true });
66
- const isDev = process.env.NODE_ENV !== 'production';
67
-
68
- if (isDev) {
69
- const vite = await createServer({ server: { middlewareMode: true } });
70
- attachVite(fastify, vite);
71
- } else {
72
- await attachRPC(fastify);
73
- fastify.register(require('@fastify/static'), { root: 'dist' });
74
- }
75
-
76
- fastify.listen({ port: 3000 });
77
- ```
78
-
79
- ## Hono
80
-
81
- ### Installation
82
-
83
- ```bash
84
- pnpm add @thednp/rpc hono
85
- ```
86
-
87
- ### Usage
88
-
89
- ```ts
90
- // server.ts
91
- import { Hono } from 'hono';
92
- import { attachRPC, attachVite } from '@thednp/rpc/hono';
93
- import { createServer } from 'vite';
94
-
95
- const app = new Hono();
96
- const isDev = process.env.NODE_ENV !== 'production';
97
-
98
- if (isDev) {
99
- const vite = await createServer({ server: { middlewareMode: true } });
100
- attachVite(app, vite);
101
- } else {
102
- await attachRPC(app);
103
- app.use('*', serveStatic({ root: './dist' }));
104
- }
105
-
106
- export default app;
107
- ```
108
-
109
- ## Koa
110
-
111
- ### Installation
112
-
113
- ```bash
114
- pnpm add @thednp/rpc koa koa-body
115
- ```
116
-
117
- ### Usage
118
-
119
- Body parser must be registered before the RPC middleware:
120
-
121
- ```ts
122
- // server.ts
123
- import Koa from 'koa';
124
- import { koaBody } from 'koa-body';
125
- import { attachRPC, attachVite } from '@thednp/rpc/koa';
126
- import { createServer } from 'vite';
127
-
128
- const app = new Koa();
129
- const isDev = process.env.NODE_ENV !== 'production';
130
-
131
- // Body parser must come before RPC middleware
132
- app.use(koaBody({ jsonLimit: 1024 * 1024 }));
133
-
134
- if (isDev) {
135
- const vite = await createServer({ server: { middlewareMode: true } });
136
- attachVite(app, vite);
137
- } else {
138
- await attachRPC(app);
139
- app.use(require('koa-static')('dist'));
140
- }
141
-
142
- app.listen(3000);
143
- ```
@@ -1,201 +0,0 @@
1
- # Best Practices
2
-
3
- ## @thednp/rpc is a Transport Pipe
4
-
5
- `@thednp/rpc` handles serialization and **transport only**. It does not provide:
6
-
7
- - Caching
8
- - Authentication
9
- - Validation
10
- - State management
11
- - Retry logic
12
-
13
- Use dedicated tools for these concerns.
14
-
15
- ## Client-Side Caching
16
-
17
- All major frameworks have a `@tanstack/<framework>-query` made by [Tanstack](https://tanstack.com/) to cover all needs except validation and authentication.
18
-
19
- For instance the `@tanstack/react-query` is the recommended layer for React apps client-side caching, invalidation, and stale-while-revalidate:
20
-
21
- ```ts
22
- // src/components/GreetUser.tsx
23
- import { useQuery, useQueryClient } from '@tanstack/react-query';
24
- import { sayHi } from '../api';
25
-
26
- function GreetUser({ name }: { name: string }) {
27
- const { data } = useQuery({
28
- queryKey: ['say-hi', name],
29
- queryFn: ({ signal }) => {
30
- const result = sayHi(name);
31
- signal.addEventListener('abort', () => result.cancel('query cancelled'));
32
- return result.data;
33
- },
34
- });
35
- return <div>{data ?? 'Loading...'}</div>;
36
- }
37
- ```
38
-
39
- ## AbortSignal Best Practices
40
-
41
- Always check `signal.aborted` or call `signal.throwIfAborted()` in long-running server functions:
42
-
43
- ```ts
44
- export const processBatch = createServerFunction(
45
- 'process-batch',
46
- async (signal: AbortSignal, items: string[]) => {
47
- const results = [];
48
- const errors = [];
49
- for (const item of items) {
50
- signal.throwIfAborted();
51
- const result = await heavyWork(item);
52
- results.push(result);
53
- // handle errors properly
54
- }
55
- return results;
56
- },
57
- );
58
- ```
59
-
60
- ## Input Validation
61
-
62
- Always validate client-provided data before using it in server functions. Use libraries like **zod** or **valibot** to parse and validate inputs:
63
-
64
- ```ts
65
- // src/api/server.ts
66
- import { z } from 'zod';
67
- import { createServerFunction } from '@thednp/rpc/server';
68
-
69
- const ProfileSchema = z.object({
70
- name: z.string().min(1).max(100),
71
- age: z.number().int().positive(),
72
- });
73
-
74
- export const updateProfile = createServerFunction(
75
- 'update-profile',
76
- async (signal, raw) => {
77
- const parsed = ProfileSchema.safeParse(raw);
78
- if (!parsed.success) {
79
- return { ok: false, errors: parsed.error.flatten().fieldErrors };
80
- }
81
- // parsed.data is fully typed
82
- await saveToDb(parsed.data);
83
- return { ok: true };
84
- },
85
- );
86
- ```
87
-
88
- Return validation errors as structured data — the client's `handleResponse` will surface them as an `Error`.
89
-
90
- ## Authentication
91
-
92
- Use middleware before `createRPCMiddleware()`:
93
-
94
- ```ts
95
- // Express
96
- app.use(authMiddleware);
97
- app.use(createRPCMiddleware());
98
- ```
99
-
100
- > Never add auth hooks inside the plugin.
101
-
102
- ## Body Limits
103
-
104
- In most cases you can rely on your framework's body-parser middleware:
105
-
106
- ```ts
107
- // Express
108
- import express from "express";
109
- const app = express();
110
- app.use(express.json({ limit: 1024 * 1024 })); // or "1mb"
111
- app.use(createRPCMiddleware());
112
- ```
113
-
114
- ```ts
115
- // Fastify
116
- import Fastify from "fastify";
117
-
118
- const app = Fastify({ logger: false, bodyLimit: 1024 * 1024 }); // "1MB"
119
- ```
120
-
121
- ```ts
122
- // Hono
123
- import { Hono } from "hono";
124
- import { bodyLimit } from 'hono/body-limit';
125
-
126
- const app = new Hono();
127
- app.use('*', bodyLimit({ maxSize: 1024 * 1024 })) // 1MB
128
- ```
129
-
130
- ```ts
131
- // Koa
132
- import Koa from "koa";
133
- import { koaBody } from 'koa-body';
134
-
135
- const app = new Koa();
136
- app.use(koaBody({ jsonLimit: 1024 * 1024 })); // 1MB
137
- ```
138
-
139
- ```ts
140
- // SSR (custom http server with Vite middleware mode)
141
- import { createMiddleware, readBody } from "@thednp/rpc/express";
142
- import { loadRPCConfig } from "@thednp/rpc";
143
-
144
- const config = await loadRPCConfig();
145
- const MAX_BODY_SIZE = 1024 * 1024;
146
-
147
- app.use(createMiddleware({
148
- rpcPreffix: config.rpcPreffix,
149
- handler: async (req, res, next) => {
150
- const { data } = await readBody(req);
151
- if (Buffer.byteLength(typeof data === "string" ? data : JSON.stringify(data)) > MAX_BODY_SIZE) {
152
- res.statusCode = 413;
153
- res.end("Payload Too Large");
154
- return;
155
- }
156
- req.body = data;
157
- next();
158
- },
159
- }));
160
- ```
161
-
162
- ```ts
163
- // SPA (dedicated RPC proxy server)
164
- import { readBody } from "@thednp/rpc/express";
165
-
166
- const MAX_BODY_SIZE = 1024 * 1024;
167
-
168
- const bodyLimit = async (req, res, next) => {
169
- const { data } = await readBody(req);
170
- if (Buffer.byteLength(typeof data === "string" ? data : JSON.stringify(data)) > MAX_BODY_SIZE) {
171
- res.statusCode = 413;
172
- res.end("Payload Too Large");
173
- return;
174
- }
175
- req.body = data;
176
- next();
177
- };
178
- ```
179
-
180
- ## SSR Guidance
181
-
182
- - On the **server**, `createServerFunction` runs directly (not Vite-transformed).
183
- - On the **client**, the plugin replaces server function calls with `fetch`-based client modules.
184
- - Keep `src/entry-client.ts` and `src/entry-server.ts` separate for proper hydration.
185
-
186
- ## File Naming
187
-
188
- Only files matching `server.ts`, `server.js`, `server.mjs`, `server.mts` in `src/api/` are scanned. Export functions individually for proper client module mapping:
189
-
190
- ```ts
191
- // ✅ Good — individual exports
192
- export const sayHi = createServerFunction('say-hi', fn);
193
- export const add = createServerFunction('add-numbers', fn);
194
-
195
- // ❌ Avoid — default export or bundled objects
196
- export default { sayHi, add };
197
- ```
198
-
199
- ## Cache is Not @thednp/rpc's Job
200
-
201
- Use `react-query`, `SWR`, or your framework's data hooks for caching. `@thednp/rpc` is the **transport layer only**.
@@ -1,62 +0,0 @@
1
- # Client Usage
2
-
3
- ## Auto-Generated Client Modules
4
-
5
- When you import from `./api` in your client code, the plugin intercepts the import and generates a client module for each server function.
6
-
7
- ```ts
8
- import { sayHi, add } from './api';
9
- ```
10
-
11
- Each imported function returns:
12
-
13
- ```ts
14
- { data: Promise<T>, cancel: (reason: string) => void }
15
- ```
16
-
17
- - **`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.
19
-
20
- ### Example
21
-
22
- ```ts
23
- import { sayHi } from './api';
24
-
25
- const { data, cancel } = sayHi('World');
26
- const result = await data; // "Hello World!"
27
- cancel('user cancelled'); // triggers AbortController on the client side
28
- ```
29
-
30
- ## Error Handling
31
-
32
- - **Fetch errors** (network failure, CORS) — thrown from `await data`
33
- - **HTTP 4xx/5xx responses** — thrown from `await data`
34
- - **Cancellation** — returns `"Request cancelled by user"` from `await data`
35
-
36
- ## @tanstack/react-query Integration
37
-
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`:
39
-
40
- ```ts
41
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
42
- import { sayHi } from './api';
43
-
44
- function GreetUser({ name }: { name: string }) {
45
- const queryClient = useQueryClient();
46
-
47
- const { data } = useQuery({
48
- queryKey: ['say-hi', name],
49
- queryFn: ({ signal }) => {
50
- const result = sayHi(name);
51
- signal.addEventListener('abort', () => result.cancel('query cancelled'));
52
- return result.data;
53
- },
54
- });
55
-
56
- return <div>{data ?? 'Loading...'}</div>;
57
- }
58
- ```
59
-
60
- Combine `cancel()` with React Query's `signal` for proper abort handling during component unmount or query invalidation.
61
-
62
- Other frameworks have a `@tanstack/<framework>-query` made by [Tanstack](https://tanstack.com/).
@@ -1,77 +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
- rpcPreffix: '__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
-
21
- ```ts
22
- import { defineConfig } from 'vite';
23
- import rpc from '@thednp/rpc';
24
-
25
- export default defineConfig({
26
- plugins: [rpc(/* development options */)]
27
- });
28
-
29
- ```
30
-
31
- > **NOTE** these plugin options only apply to **development** and override the options in `rpc.config.ts`.
32
-
33
- ### Options
34
-
35
- | Option | Type | Default | Description |
36
- | --------------| ----------| -------------| --------------------------------------------------------------|
37
- | `rpcPreffix` | `string` | `'__rpc'` | RPC endpoint prefix used in URL routing |
38
- | `adapter` | `string` | `'express'` | Target adapter (`'express'`, `'fastify'`, `'hono'`, `'koa'`) |
39
-
40
- ## Config File Discovery
41
-
42
- When `configFile` is not specified, the plugin searches for config files in this order:
43
-
44
- 1. `rpc.config.ts`
45
- 2. `rpc.config.js`
46
- 3. `rpc.config.mjs`
47
- 4. `rpc.config.mts`
48
- 5. `.rpcrc.ts`
49
- 6. `.rpcrc.js`
50
-
51
- The first file found is used. If none is found, defaults are applied.
52
-
53
- ## Utilities
54
-
55
- ### `defineConfig`
56
-
57
- Type-safe helper for creating the config object. Provides autocomplete and type checking for all options.
58
-
59
- ```ts
60
- import { defineConfig } from '@thednp/rpc';
61
-
62
- export default defineConfig({
63
- rpcPreffix: '__rpc',
64
- });
65
- ```
66
-
67
- ### `loadRPCConfig`
68
-
69
- Programmatically load the RPC config, useful in custom server setups:
70
-
71
- ```ts
72
- import { loadRPCConfig } from '@thednp/rpc';
73
-
74
- const config = await loadRPCConfig();
75
- console.log(config.rpcPreffix); // '__rpc'
76
- console.log(config.adapter); // 'express'
77
- ```
@@ -1,76 +0,0 @@
1
- # Getting Started
2
-
3
- ## Installation
4
-
5
- ```bash
6
- pnpm add @thednp/rpc@latest
7
- ```
8
-
9
- ```bash
10
- npm install @thednp/rpc@latest
11
- ```
12
-
13
- ```bash
14
- bun add @thednp/rpc@latest
15
- ```
16
-
17
- ```bash
18
- deno add npm:@thednp/rpc@latest
19
- ```
20
-
21
- ## Quick Start
22
-
23
- ### 1. Configure system wide configuration `rpc.config.ts`
24
-
25
- ```ts
26
- import { defineConfig } from "@thednp/rpc";
27
-
28
- export default defineConfig({
29
- rpcPreffix: "__server",
30
- adapter: "express",
31
- });
32
- ```
33
-
34
- Currently `@thednp/rpc` supports `'express'`, `'fastify'`, `'hono'` and `'koa'`. Check [adapters](./adapters.md) for more guides.
35
- Also check [configuration](./configuration.md) for more guides.
36
-
37
-
38
- ### 2. Add the plugin to `vite.config.ts`
39
-
40
- ```ts
41
- import rpc from '@thednp/rpc';
42
-
43
- export default {
44
- plugins: [rpc()],
45
- };
46
- ```
47
-
48
- Check [configuration](./configuration.md) for more guides.
49
-
50
-
51
- ### 3. Create a server function in `src/api/server.ts`
52
-
53
- ```ts
54
- import { createServerFunction } from '@thednp/rpc/server';
55
-
56
- export const sayHi = createServerFunction(
57
- 'say-hi',
58
- async (signal: AbortSignal, name: string) => {
59
- signal.throwIfAborted();
60
- await new Promise((res) => setTimeout(res, 1500));
61
- return `Hello ${name}!`;
62
- },
63
- );
64
- ```
65
-
66
- ### 4. Use it on the client
67
-
68
- ```ts
69
- import { sayHi } from './api';
70
-
71
- const { data, cancel } = sayHi('World');
72
- const result = await data; // "Hello World!"
73
- cancel('user cancelled');
74
- ```
75
-
76
- 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.
package/wiki/index.md DELETED
@@ -1,26 +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
- ## 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
- ## Table of Contents
18
-
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
package/wiki/security.md DELETED
@@ -1,54 +0,0 @@
1
- # Security
2
-
3
- ## Prefix Boundary Check
4
-
5
- All adapters use `new RegExp(\`^/${rpcPreffix}/\`)` instead of `startsWith` to match the RPC endpoint path. This prevents path-segment bypass attacks:
6
-
7
- ```
8
- rpcPreffix = '__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
- ## Authentication via Middleware
32
-
33
- Authentication is handled by middleware registered **before** `createRPCMiddleware()`. The middleware chain composes naturally:
34
-
35
- ```ts
36
- // Express example
37
- app.use(authMiddleware); // auth first
38
- app.use(createRPCMiddleware()); // RPC second
39
- ```
40
-
41
- Do not add auth hooks inside the plugin. Use your framework's standard middleware pattern.
42
-
43
- ## Body Size Limits
44
-
45
- JSON body size limits are handled by your framework's body-parser middleware:
46
-
47
- - **Express**: `express.json({ limit: '1mb' })` (default **100kb**)
48
- - **Fastify**: `bodyLimit` option in Fastify config (default **1 MiB**)
49
- - **Koa**: `koa-body({ formLimit: '1mb' })`
50
- - **Hono**: Built-in body size limiting
51
-
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.
53
-
54
- Register body parsing middleware before `createRPCMiddleware()`.