@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,137 +0,0 @@
1
- # Server Functions
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
-
13
- ## `createServerFunction(name, handler, options?)`
14
-
15
- The core API for defining server-side functions.
16
-
17
- ### Signature
18
-
19
- ```ts
20
- function createServerFunction<T>(
21
- name: string,
22
- handler: (signal: AbortSignal, ...args: JsonArray) => Promise<T>,
23
- options?: {
24
- contentType?: 'application/json' | 'text/plain',
25
- credentials?: "same-origin" | "include" | "omit",
26
- method?: "GET" | "POST",
27
- }
28
- ): ServerFunction<T>;
29
- ```
30
-
31
- ### Parameters
32
-
33
- - **`name`** (`string`) — The registered name used in RPC routing.
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.
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.
66
-
67
- ### AbortSignal
68
-
69
- The first argument to every server function is an `AbortSignal`. This allows the client to cancel a request:
70
-
71
- ```ts
72
- export const longTask = createServerFunction(
73
- 'long-task',
74
- async (signal: AbortSignal, id: string) => {
75
- signal.throwIfAborted(); // throws if client cancelled
76
- // ... do work ...
77
- signal.throwIfAborted(); // check again after each step
78
- return result;
79
- },
80
- );
81
- ```
82
-
83
- Use `signal.aborted` or `signal.throwIfAborted()` in long-running functions to respond to cancellation promptly.
84
-
85
- ### Registration
86
-
87
- When `createServerFunction` is called, it registers the function in a server-side map keyed by `name`. This map is used by the RPC middleware to route incoming requests to the correct implementation.
88
-
89
- ### Return Type
90
-
91
- The return value of `handler` is serialized to JSON and sent as the HTTP response body. **Ensure your return type is JSON-serializable.**
92
-
93
- ## Input Validation
94
-
95
- Server functions receive raw, untrusted client data. **Always validate data within your server functions before use.**
96
-
97
- **zod:**
98
-
99
- ```ts
100
- import { z } from 'zod';
101
- import { createServerFunction } from '@thednp/rpc/server';
102
-
103
- const AddSchema = z.object({
104
- a: z.number(),
105
- b: z.number(),
106
- });
107
-
108
- export const add = createServerFunction('add', async (signal, raw) => {
109
- const parsed = AddSchema.safeParse(raw);
110
- if (!parsed.success) {
111
- return { error: parsed.error.flatten() };
112
- }
113
- return parsed.data.a + parsed.data.b;
114
- });
115
- ```
116
-
117
- **valibot:**
118
-
119
- ```ts
120
- import * as v from 'valibot';
121
- import { createServerFunction } from '@thednp/rpc/server';
122
-
123
- const AddSchema = v.object({
124
- a: v.number(),
125
- b: v.number(),
126
- });
127
-
128
- export const add = createServerFunction('add', async (signal, raw) => {
129
- const parsed = v.safeParse(AddSchema, raw);
130
- if (parsed.issues) {
131
- return { error: v.flatten(parsed.issues).nested };
132
- }
133
- return parsed.output.a + parsed.output.b;
134
- });
135
- ```
136
-
137
- Validation errors return structured data instead of throwing — the client's auto-generated `handleResponse` receives `{ error: ... }` and surfaces it as an `Error`.
package/wiki/setup.md DELETED
@@ -1,81 +0,0 @@
1
- # Setup
2
-
3
- ## Required Project Structure
4
-
5
- ```
6
- project/
7
- ├── src/
8
- │ ├── api/
9
- │ │ └── index.ts # All server functions exports
10
- │ │ └── server.ts # Auto-scanned server functions
11
- │ ├── entry-client.ts # Client entry (SSR projects)
12
- │ └── entry-server.ts # Server entry (SSR projects)
13
- ├── vite.config.ts # Add rpc() plugin here
14
- ├── rpc.config.ts # Optional config
15
- ├── package.json # The project npm configuration
16
- └── server.js # Your Express/Hono/Fastify/Koa server
17
- ```
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
-
21
- ## Server Files
22
-
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):
24
-
25
- - `server.ts`
26
- - `server.js`
27
- - `server.mjs`
28
- - `server.mts`
29
-
30
- Each matched file is loaded with `vite.ssrLoadModule`, and all named exports are mapped to client functions. Export each function individually for proper mapping.
31
-
32
- ## How Auto-Scanning Works
33
-
34
- 1. During `resolveId`, the plugin intercepts imports from `./api` (or paths under `src/api/`).
35
- 2. It scans `src/api/` for the server files listed above and loads them via `vite.ssrLoadModule`.
36
- 3. It builds a map of export names to their `createServerFunction` registration names.
37
- 4. During `transform`, it replaces the import with generated client modules that use `fetch` API under the hood.
38
-
39
- ## SSR vs SPA
40
-
41
- ### SSR Projects
42
-
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).
44
-
45
- ### SPA Projects
46
-
47
- Import directly from `./api` in your client code. No server entry is needed.
48
-
49
- ## Production Adapters
50
-
51
- Use the appropriate adapter for your framework:
52
-
53
- ```ts
54
- // Express
55
- import { attachRPC, attachVite } from '@thednp/rpc/express';
56
- await attachRPC(app); // production
57
- attachVite(app, vite); // development
58
- ```
59
-
60
- ```ts
61
- // Fastify
62
- import { attachRPC, attachVite } from '@thednp/rpc/fastify';
63
- await attachRPC(app); // production
64
- attachVite(app, vite); // development
65
- ```
66
-
67
- ```ts
68
- // Hono
69
- import { attachRPC, attachVite } from '@thednp/rpc/hono';
70
- await attachRPC(app); // production
71
- attachVite(app, vite); // development
72
- ```
73
-
74
- ```ts
75
- // Koa
76
- import { attachRPC, attachVite } from '@thednp/rpc/koa';
77
- await attachRPC(app); // production
78
- attachVite(app, vite); // development
79
- ```
80
-
81
- See [Adapters](./adapters.md) for full server setup examples.