@atom-forge/rpc 0.3.2

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 (42) hide show
  1. package/LICENSE +28 -0
  2. package/README.en.md +612 -0
  3. package/README.hu.md +613 -0
  4. package/README.llm.md +343 -0
  5. package/README.md +25 -0
  6. package/dist/client/client-context.d.ts +45 -0
  7. package/dist/client/client-context.js +48 -0
  8. package/dist/client/create-client.d.ts +9 -0
  9. package/dist/client/create-client.js +277 -0
  10. package/dist/client/logger.d.ts +6 -0
  11. package/dist/client/logger.js +41 -0
  12. package/dist/client/middleware.d.ts +6 -0
  13. package/dist/client/middleware.js +7 -0
  14. package/dist/client/rpc-response.d.ts +27 -0
  15. package/dist/client/rpc-response.js +46 -0
  16. package/dist/client/types.d.ts +151 -0
  17. package/dist/client/types.js +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.js +7 -0
  20. package/dist/server/create-handler.d.ts +18 -0
  21. package/dist/server/create-handler.js +210 -0
  22. package/dist/server/errors.d.ts +10 -0
  23. package/dist/server/errors.js +14 -0
  24. package/dist/server/middleware.d.ts +22 -0
  25. package/dist/server/middleware.js +39 -0
  26. package/dist/server/rpc.d.ts +65 -0
  27. package/dist/server/rpc.js +49 -0
  28. package/dist/server/server-context.d.ts +79 -0
  29. package/dist/server/server-context.js +86 -0
  30. package/dist/server/types.d.ts +30 -0
  31. package/dist/server/types.js +1 -0
  32. package/dist/util/constants.d.ts +1 -0
  33. package/dist/util/constants.js +1 -0
  34. package/dist/util/cookies.d.ts +22 -0
  35. package/dist/util/cookies.js +54 -0
  36. package/dist/util/pipeline.d.ts +23 -0
  37. package/dist/util/pipeline.js +22 -0
  38. package/dist/util/string.d.ts +6 -0
  39. package/dist/util/string.js +11 -0
  40. package/dist/util/types.d.ts +5 -0
  41. package/dist/util/types.js +1 -0
  42. package/package.json +38 -0
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ AtomForge — Patron License
2
+ Copyright (c) 2024-present Elvis Szabo
3
+
4
+ Permission is hereby granted, free of charge, to any individual or
5
+ non-commercial organization obtaining a copy of this software (the "Software"),
6
+ to use, copy, modify, and distribute the Software for non-commercial purposes,
7
+ subject to the following conditions:
8
+
9
+ NON-COMMERCIAL USE
10
+ The above rights are granted at no charge for:
11
+ - Personal and hobby projects
12
+ - Open source projects
13
+ - Non-profit organizations
14
+
15
+ COMMERCIAL USE
16
+ Any use of the Software in a for-profit context requires a paid commercial
17
+ license. A commercial license is granted to any individual or entity that
18
+ actively supports the project via GitHub Sponsors
19
+ (https://github.com/sponsors/atom-forge).
20
+
21
+ The license is perpetual for projects initiated during the active support
22
+ period. Stopping support does not revoke the license for projects already
23
+ in production at the time.
24
+
25
+ DISCLAIMER
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28
+ OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,612 @@
1
+ # Rpc
2
+
3
+ Rpc is a full-stack RPC (Remote Procedure Call) framework for TypeScript projects. It simplifies the communication between the client and the server by providing a type-safe API. **Framework-agnostic** — works with any Node.js or edge runtime (SvelteKit, Express, Hono, Next.js, Nuxt, etc.).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @atom-forge/rpc
9
+ pnpm add @atom-forge/rpc
10
+ yarn add @atom-forge/rpc
11
+ bun add @atom-forge/rpc
12
+ ```
13
+
14
+ ## Core Concept: End-to-End Type Safety
15
+
16
+ Rpc's main feature is providing end-to-end type safety between your server and client. You define your API on the server, then share the type of that definition with the client. This gives you autocompletion and type checking for your API calls.
17
+
18
+ **1. Define your API on the server:**
19
+
20
+ ```typescript
21
+ // api.ts (shared API definition)
22
+ import { rpc } from '@atom-forge/rpc';
23
+
24
+ export const api = {
25
+ posts: {
26
+ list: rpc.query(async ({ page }: { page: number }, ctx) => {
27
+ // ... fetch posts
28
+ return { posts: [{ id: 1, title: 'Hello' }] };
29
+ }),
30
+ create: rpc.command(async ({ title }: { title: string }) => {
31
+ // ... create post
32
+ return { success: true };
33
+ }),
34
+ },
35
+ };
36
+ ```
37
+
38
+ ```typescript
39
+ // SvelteKit: src/routes/rpc/[...path]/+server.ts
40
+ import { createCoreHandler, flattenApiDefinition } from '@atom-forge/rpc';
41
+ import { api } from '$lib/api';
42
+
43
+ const handle = createCoreHandler(flattenApiDefinition(api));
44
+
45
+ export const GET = (event) => handle(event.request, { path: event.params.path }, event);
46
+ export const POST = GET;
47
+ ```
48
+
49
+ **2. Use the type on the client:**
50
+
51
+ ```typescript
52
+ // src/lib/client/rpc.ts
53
+ import { createClient } from '@atom-forge/rpc';
54
+ import type { api } from '$lib/api';
55
+
56
+ const [client, cfg] = createClient<typeof api>('/rpc');
57
+
58
+ // Every call returns a RpcResponse
59
+ const res = await client.posts.list.$query({ page: 1 });
60
+ if (res.isOK()) {
61
+ console.log(res.result); // typed as { posts: { id: number, title: string }[] }
62
+ }
63
+
64
+ await client.posts.create.$command({ title: 'My New Post' });
65
+
66
+ export default client;
67
+ ```
68
+
69
+ ## Framework Adapters
70
+
71
+ The `createCoreHandler` function works on standard `Request` → `Response`. Each framework needs ~2–5 lines of adapter code.
72
+
73
+ ### SvelteKit
74
+
75
+ ```typescript
76
+ // src/routes/rpc/[...path]/+server.ts
77
+ import { createCoreHandler, flattenApiDefinition } from '@atom-forge/rpc';
78
+ import { api } from '$lib/api';
79
+
80
+ const handle = createCoreHandler(flattenApiDefinition(api));
81
+
82
+ export const GET = (event) => handle(event.request, { path: event.params.path }, event);
83
+ export const POST = GET;
84
+ ```
85
+
86
+ In SvelteKit, `ctx.adapterContext` is the `RequestEvent`, giving access to `locals`, `platform`, etc.:
87
+
88
+ ```typescript
89
+ // ctx.adapterContext type: RequestEvent
90
+ const user = (ctx.adapterContext as RequestEvent).locals.user;
91
+ ```
92
+
93
+ **Alternative: `hooks.server.ts`**
94
+
95
+ Instead of a route file, you can intercept RPC requests directly in the server hook — useful if you already have a `hooks.server.ts` or prefer to keep all server logic in one place:
96
+
97
+ ```typescript
98
+ // src/hooks.server.ts
99
+ import { createCoreHandler, flattenApiDefinition } from '@atom-forge/rpc';
100
+ import { api } from '$lib/api';
101
+
102
+ const handleRpc = createCoreHandler(flattenApiDefinition(api));
103
+
104
+ export const handle = async ({ event, resolve }) => {
105
+ if (event.url.pathname.startsWith('/rpc/')) {
106
+ const path = event.url.pathname.slice('/rpc/'.length);
107
+ return handleRpc(event.request, { path }, event);
108
+ }
109
+ return resolve(event);
110
+ };
111
+ ```
112
+
113
+ No route file needed. The hook runs before SvelteKit's router, so it's marginally faster and doesn't require a `src/routes/rpc/` directory.
114
+
115
+ ### Express
116
+
117
+ ```typescript
118
+ import { createCoreHandler, flattenApiDefinition } from '@atom-forge/rpc';
119
+ import { api } from './api';
120
+
121
+ const handle = createCoreHandler(flattenApiDefinition(api));
122
+
123
+ app.all('/rpc/:path', async (req, res) => {
124
+ const request = new Request(
125
+ `${req.protocol}://${req.get('host')}${req.originalUrl}`,
126
+ { method: req.method, headers: req.headers as any, body: req.method !== 'GET' ? req : null }
127
+ );
128
+ const response = await handle(request, { path: req.params.path }, { req, res });
129
+ res.status(response.status);
130
+ response.headers.forEach((v, k) => res.setHeader(k, v));
131
+ res.send(Buffer.from(await response.arrayBuffer()));
132
+ });
133
+ ```
134
+
135
+ ### Hono
136
+
137
+ ```typescript
138
+ import { createCoreHandler, flattenApiDefinition } from '@atom-forge/rpc';
139
+ import { api } from './api';
140
+
141
+ const handle = createCoreHandler(flattenApiDefinition(api));
142
+
143
+ app.all('/rpc/:path', (c) => handle(c.req.raw, { path: c.req.param('path') }, c));
144
+ ```
145
+
146
+ ### Next.js (App Router)
147
+
148
+ ```typescript
149
+ // app/rpc/[...path]/route.ts
150
+ import { createCoreHandler, flattenApiDefinition } from '@atom-forge/rpc';
151
+ import { api } from '@/lib/api';
152
+
153
+ const handle = createCoreHandler(flattenApiDefinition(api));
154
+
155
+ export async function GET(request: Request, { params }: { params: Promise<{ path: string[] }> }) {
156
+ const { path } = await params;
157
+ return handle(request, { path: path.join('.') }, { request, params });
158
+ }
159
+ export const POST = GET;
160
+ ```
161
+
162
+ > `path.join('.')` reassembles URL segments (`['users', 'get-all']`) into the dot-separated path (`'users.get-all'`). In Next.js 15+, `params` is a Promise — hence the `await`.
163
+
164
+ ### Nuxt 3
165
+
166
+ ```typescript
167
+ // server/routes/rpc/[...path].ts
168
+ import { createCoreHandler, flattenApiDefinition } from '@atom-forge/rpc';
169
+ import { getRouterParam, toWebRequest } from 'h3';
170
+ import { api } from '~/lib/api';
171
+
172
+ const handle = createCoreHandler(flattenApiDefinition(api));
173
+
174
+ export default defineEventHandler(async (event) => {
175
+ const path = getRouterParam(event, 'path') ?? '';
176
+ return handle(toWebRequest(event), { path }, event);
177
+ });
178
+ ```
179
+
180
+ > `toWebRequest()` converts the h3 event into a standard `Request`. `defineEventHandler` natively accepts a `Response` return value.
181
+
182
+ ## Communication Protocol
183
+
184
+ Rpc uses **MessagePack** as its primary communication protocol for efficiency and performance. For clients that do not support MessagePack, it can fall back to **JSON**.
185
+
186
+ - **`$command`**: Sends data in the request body, encoded with MessagePack (`application/msgpack`). Plain JSON (`application/json`) is also accepted by the server.
187
+ - **`$query`**: Sends data in the URL's query string, encoded with MessagePack and Base64. This is the recommended method for queries.
188
+ - **`$get`**: Sends data as plain text in the URL's query string. Useful for clients that do not support MessagePack, or for simple non-complex queries.
189
+
190
+ The server automatically detects the client's `Accept` header and responds with either MessagePack or JSON.
191
+
192
+ ### Response Headers
193
+
194
+ Every response includes the following headers:
195
+
196
+ | Header | Description |
197
+ |---|---|
198
+ | `x-atom-forge-rpc-exec-time` | Server-side execution time in milliseconds. |
199
+
200
+ ## Client-side Usage
201
+
202
+ ### `createClient`
203
+
204
+ The `createClient` function creates a new API client. The way you call an endpoint on the client (`$query` or `$get`) must match how it was defined on the server (`rpc.query` or `rpc.get`).
205
+
206
+ ```typescript
207
+ import { createClient } from '@atom-forge/rpc';
208
+ import type { api } from './api';
209
+
210
+ const [client, cfg] = createClient<typeof api>('/rpc');
211
+
212
+ // If the server endpoint is defined with rpc.query:
213
+ const result = await client.posts.list.$query({ page: 1 });
214
+
215
+ // Command call
216
+ await client.posts.create.$command({ title: 'Hello World' });
217
+ ```
218
+
219
+ ### Call Options
220
+
221
+ Every RPC method (`$command`, `$query`, `$get`) accepts an optional second argument with per-call options:
222
+
223
+ ```typescript
224
+ const result = await client.posts.list.$query({ page: 1 }, {
225
+ // Abort the request using an AbortController
226
+ abortSignal: controller.signal,
227
+
228
+ // Track upload/download progress (uses XHR internally)
229
+ onProgress: ({ loaded, total, percent, phase }) => {
230
+ console.log(`${phase}: ${percent}%`);
231
+ },
232
+
233
+ // Add custom request headers for this call only
234
+ headers: new Headers({ 'X-Custom-Header': 'value' }),
235
+ });
236
+ ```
237
+
238
+ ### `RpcResponse`
239
+
240
+ Every RPC call returns a `RpcResponse` with these members:
241
+
242
+ | Member | Description |
243
+ |---|---|
244
+ | `res.isOK()` | `true` if the call succeeded |
245
+ | `res.isError(code?)` | `true` if error; optionally checks a specific code |
246
+ | `res.status` | `'OK'` on success, or the error code string |
247
+ | `res.result` | Typed success data, or error details |
248
+ | `res.ctx` | The full `ClientContext` for this call |
249
+
250
+ **Error code format:**
251
+ - Application-level errors: `'INVALID_ARGUMENT'`, `'PERMISSION_DENIED'`, custom codes, etc.
252
+ - Transport errors: `'HTTP:401'`, `'HTTP:404'`, `'HTTP:500'`, etc.
253
+ - Network errors: `'NETWORK_ERROR'`
254
+
255
+ ```typescript
256
+ const res = await client.posts.create.$command({ title: 'Hello' });
257
+
258
+ if (res.isOK()) {
259
+ console.log(res.result); // typed result
260
+ } else if (res.isError('INVALID_ARGUMENT')) {
261
+ console.log(res.result.message); // error details
262
+ } else if (res.isError('HTTP:401')) {
263
+ // redirect to login
264
+ } else {
265
+ console.log(res.status, res.result); // any other error
266
+ }
267
+
268
+ // Access context (response headers, elapsed time, etc.)
269
+ console.log(res.ctx.response?.status);
270
+ console.log(res.ctx.elapsedTime);
271
+ ```
272
+
273
+ ### File Uploads
274
+
275
+ `$command` endpoints automatically detect `File` or `File[]` values in the arguments and switch to a `multipart/form-data` request. You can combine file uploads with regular arguments and track progress.
276
+
277
+ ```typescript
278
+ // Server-side
279
+ const api = {
280
+ posts: {
281
+ create: rpc.command(async ({ title, cover }: { title: string; cover: File }) => {
282
+ // cover is a File object
283
+ }),
284
+ },
285
+ };
286
+
287
+ // Client-side
288
+ const coverFile = document.querySelector('input[type=file]').files[0];
289
+
290
+ await client.posts.create.$command(
291
+ { title: 'Hello', cover: coverFile },
292
+ {
293
+ onProgress: ({ percent, phase }) => console.log(`${phase}: ${percent}%`),
294
+ }
295
+ );
296
+ ```
297
+
298
+ For multiple files, use an array and suffix the key with `[]`:
299
+
300
+ ```typescript
301
+ // Server-side
302
+ const api = {
303
+ media: {
304
+ upload: rpc.command(async ({ files }: { files: File[] }) => { ... }),
305
+ },
306
+ };
307
+
308
+ // Client-side
309
+ await client.media.upload.$command({ 'files[]': selectedFiles });
310
+ ```
311
+
312
+ ### `clientLogger`
313
+
314
+ `clientLogger` is a built-in client middleware that logs RPC call details to the browser console — including the request path, arguments, response, timing, and HTTP status code.
315
+
316
+ ```typescript
317
+ import { createClient, clientLogger } from '@atom-forge/rpc';
318
+
319
+ const [client, cfg] = createClient<typeof api>('/rpc');
320
+ cfg.$ = clientLogger('/rpc'); // apply globally
321
+ ```
322
+
323
+ ### `makeClientMiddleware`
324
+
325
+ The `makeClientMiddleware` function is used to create a client-side middleware.
326
+
327
+ > ⚠️ **Always `return await next()`** in your middleware. If you call `next()` without returning its result, the response will be lost and the caller will receive `undefined`.
328
+
329
+ ```typescript
330
+ import { makeClientMiddleware } from '@atom-forge/rpc';
331
+
332
+ const loggerMiddleware = makeClientMiddleware(async (ctx, next) => {
333
+ console.log('Request:', ctx.path, ctx.getArgs());
334
+ const result = await next(); // ✅ always return the result of next()
335
+ console.log('Response:', ctx.result);
336
+ return result;
337
+ });
338
+
339
+ // Apply middleware to all routes
340
+ cfg.$ = loggerMiddleware;
341
+ ```
342
+
343
+ ## Server-side Usage
344
+
345
+ ### `createCoreHandler` and `flattenApiDefinition`
346
+
347
+ `createCoreHandler` creates a framework-agnostic handler that works on standard `Request` → `Response`. `flattenApiDefinition` prepares the API definition for the handler.
348
+
349
+ ```typescript
350
+ import { createCoreHandler, flattenApiDefinition, rpc } from '@atom-forge/rpc';
351
+
352
+ const api = {
353
+ posts: {
354
+ // expects $query from the client
355
+ list: rpc.query(async ({ page }, ctx) => {
356
+ ctx.cache.set(60);
357
+ return { posts: [] };
358
+ }),
359
+ // expects $get from the client
360
+ getById: rpc.get(async ({ id }, ctx) => {
361
+ return { id, title: 'Example Post' };
362
+ }),
363
+ // expects $command from the client
364
+ create: rpc.command(async ({ title }) => {
365
+ // create a new post
366
+ }),
367
+ },
368
+ };
369
+
370
+ const handle = createCoreHandler(flattenApiDefinition(api));
371
+ ```
372
+
373
+ #### Custom Server Context
374
+
375
+ You can provide a custom server context factory to inject your own properties (e.g. authenticated user) into every handler:
376
+
377
+ ```typescript
378
+ import { createCoreHandler, flattenApiDefinition, ServerContext } from '@atom-forge/rpc';
379
+ import type { RequestEvent } from '@sveltejs/kit';
380
+
381
+ class AppContext extends ServerContext<RequestEvent> {
382
+ get user() {
383
+ return this.adapterContext.locals.user;
384
+ }
385
+ }
386
+
387
+ const handle = createCoreHandler(flattenApiDefinition(api), {
388
+ createServerContext: (args, request, adapterContext) =>
389
+ new AppContext(args, request, adapterContext),
390
+ });
391
+ ```
392
+
393
+ ### The `rpc` object
394
+
395
+ The `rpc` object provides methods for defining your API endpoints. The method you use on the server determines how the client must call the endpoint.
396
+
397
+ * `rpc.query`: Defines a query endpoint that expects arguments encoded with MessagePack. The client must use **`$query`**.
398
+ * `rpc.get`: Defines a query endpoint that expects arguments as plain text in the URL. The client must use **`$get`**.
399
+ * `rpc.command`: Defines a command endpoint. The client must use **`$command`**.
400
+
401
+ #### `rpcFactory`
402
+
403
+ If you use a custom server context (see above), use `rpcFactory` to create a typed `rpc` instance so that `ctx` is properly typed in your handlers:
404
+
405
+ ```typescript
406
+ import { rpcFactory } from '@atom-forge/rpc';
407
+
408
+ const rpc = rpcFactory<AppContext>();
409
+
410
+ const api = {
411
+ posts: {
412
+ list: rpc.query(async ({ page }, ctx) => {
413
+ // ctx is typed as AppContext
414
+ const user = ctx.user;
415
+ return { posts: [] };
416
+ }),
417
+ },
418
+ };
419
+ ```
420
+
421
+ ### Server Context (`ctx`)
422
+
423
+ Every handler and server-side middleware receives a `ctx` object with the following members:
424
+
425
+ | Member | Description |
426
+ |---|---|
427
+ | `ctx.request` | The standard Web API `Request` object. |
428
+ | `ctx.adapterContext` | The framework-specific context (SvelteKit: `RequestEvent`, Hono: `Context`, etc.). |
429
+ | `ctx.getArgs()` | Returns all arguments as a plain object. |
430
+ | `ctx.args` | The arguments as a `Map<string, any>`. |
431
+ | `ctx.cookies` | Cookie manager: `get(name)`, `set(name, value, opts?)`, `delete(name, opts?)`, `getAll()`. |
432
+ | `ctx.headers.request` | The incoming request headers. |
433
+ | `ctx.headers.response` | The mutable response headers. |
434
+ | `ctx.cache.set(seconds)` | Sets the `Cache-Control` max-age for GET responses. |
435
+ | `ctx.cache.get()` | Returns the current cache duration. |
436
+ | `ctx.status.set(code)` | Sets the HTTP response status code. |
437
+ | `ctx.status.notFound()` | Shorthand for common HTTP codes (see below). |
438
+ | `ctx.env` | A `Map<string\|symbol, any>` for passing data between middlewares. |
439
+ | `ctx.elapsedTime` | Server-side elapsed time in milliseconds. |
440
+
441
+ **Status shortcuts:** `ok`, `created`, `accepted`, `noContent`, `badRequest`, `unauthorized`, `forbidden`, `notFound`, `methodNotAllowed`, `conflict`, `tooManyRequests`, `serverError`, `serviceUnavailable`, and more.
442
+
443
+ ### Caching
444
+
445
+ Rpc supports server-side caching for `GET` requests (both `rpc.query` and `rpc.get`). Set the cache duration in seconds using `ctx.cache.set()` within your endpoint implementation.
446
+
447
+ ```typescript
448
+ const api = {
449
+ posts: {
450
+ list: rpc.query(async ({ page }, ctx) => {
451
+ ctx.cache.set(60); // Cache the response for 60 seconds
452
+ return { posts: [] };
453
+ }),
454
+ },
455
+ };
456
+ ```
457
+
458
+ ### Error Handling
459
+
460
+ Use the built-in error helpers to return application-level errors from handlers. These always produce a `200 OK` response with the `atomforge.rpc.error` key, so the client receives a typed `RpcResponse`.
461
+
462
+ ```typescript
463
+ import { rpc } from '@atom-forge/rpc';
464
+
465
+ const api = {
466
+ posts: {
467
+ create: rpc.command(async ({ title }, ctx) => {
468
+ // SvelteKit: (ctx.adapterContext as RequestEvent).locals.user
469
+ if (!ctx.adapterContext?.locals?.user) return rpc.error.permissionDenied();
470
+ if (title.length < 3) return rpc.error.invalidArgument({ message: 'Title too short' });
471
+ // ...
472
+ return { id: 1, title };
473
+ }),
474
+ },
475
+ };
476
+ ```
477
+
478
+ Use `rpc.error.make` for custom error codes:
479
+
480
+ ```typescript
481
+ return rpc.error.make('POST_ALREADY_EXISTS', 'This slug already exists', { slug: post.slug });
482
+ ```
483
+
484
+ | Method | Error code | Use when |
485
+ |---|---|---|
486
+ | `rpc.error.invalidArgument(details?)` | `INVALID_ARGUMENT` | Business logic validation (beyond Zod) |
487
+ | `rpc.error.permissionDenied(details?)` | `PERMISSION_DENIED` | Authorization failure |
488
+ | `rpc.error.internalError(details?)` | `INTERNAL_ERROR` | Handled internal failure (auto `correlationId`) |
489
+ | `rpc.error.make(code, message?, result?)` | custom | Any custom error code |
490
+
491
+ ### `zod` integration
492
+
493
+ Rpc has built-in support for `zod` for input validation. Install `zod` as a dependency of your project and import it directly.
494
+
495
+ If validation fails, Rpc automatically returns an application-level error (`200 OK`) with code `INVALID_ARGUMENT` and the `ZodIssue` array in the `issues` field. The handler does not run.
496
+
497
+ ```typescript
498
+ // Server-side
499
+ import { rpc } from '@atom-forge/rpc';
500
+ import { z } from 'zod';
501
+
502
+ const api = {
503
+ posts: {
504
+ create: rpc.zod({
505
+ title: z.string().min(3, "Title must be at least 3 characters long."),
506
+ content: z.string().min(10),
507
+ }).command(async ({ title, content }) => {
508
+ // This code only runs if validation passes
509
+ }),
510
+ },
511
+ };
512
+ ```
513
+
514
+ `rpc.zod` also works with `query` and `get`:
515
+
516
+ ```typescript
517
+ rpc.zod({ id: z.number() }).query(async ({ id }, ctx) => { ... })
518
+ rpc.zod({ id: z.number() }).get(async ({ id }, ctx) => { ... })
519
+ ```
520
+
521
+ Handle validation errors on the client via `RpcResponse`:
522
+
523
+ ```typescript
524
+ // Client-side
525
+ const res = await client.posts.create.$command({ title: 'Hi' });
526
+ if (res.isError('INVALID_ARGUMENT')) {
527
+ console.log(res.result.issues); // ZodIssue[]
528
+ }
529
+ ```
530
+
531
+ ### `makeServerMiddleware`
532
+
533
+ The `makeServerMiddleware` function is used to create server-side middleware. An optional second argument lets you attach accessor functions to the middleware, which is useful for creating reusable, self-contained middleware with helpers.
534
+
535
+ > ⚠️ **Always `return await next()`** in your middleware. If you call `next()` without returning its result, the handler's return value will be lost and the client will receive `undefined`.
536
+
537
+ ```typescript
538
+ import { makeServerMiddleware } from '@atom-forge/rpc';
539
+ import type { RequestEvent } from '@sveltejs/kit';
540
+
541
+ const authMiddleware = makeServerMiddleware(
542
+ async (ctx, next) => {
543
+ const user = (ctx.adapterContext as RequestEvent).locals.user;
544
+ if (!user) {
545
+ ctx.status.unauthorized();
546
+ return { error: 'Unauthorized' }; // ✅ early return, no next() call needed
547
+ }
548
+ return await next(); // ✅ always return the result of next()
549
+ },
550
+ // Optional accessors attached to the middleware function itself
551
+ {
552
+ isAdmin: (ctx) => (ctx.adapterContext as RequestEvent).locals.user?.role === 'admin',
553
+ }
554
+ );
555
+ ```
556
+
557
+ The accessor functions are attached directly to the middleware function object, keeping the middleware and its associated helpers co-located. Call them from within endpoint implementations by passing `ctx`:
558
+
559
+ ```typescript
560
+ const api = {
561
+ admin: {
562
+ deletePost: rpc.middleware(authMiddleware).command(async ({ id }, ctx) => {
563
+ if (!authMiddleware.isAdmin(ctx)) {
564
+ ctx.status.forbidden();
565
+ return { error: 'Admin only' };
566
+ }
567
+ // proceed...
568
+ }),
569
+ },
570
+ };
571
+ ```
572
+
573
+ This pattern keeps the middleware's knowledge — what constitutes an `isAdmin` check — in one place rather than repeating the logic in every endpoint.
574
+
575
+ ### Applying Middleware with `rpc.middleware`
576
+
577
+ Use `rpc.middleware()` to attach one or more server middlewares to an endpoint:
578
+
579
+ ```typescript
580
+ import { rpc } from '@atom-forge/rpc';
581
+ import { z } from 'zod';
582
+
583
+ // Apply middleware to a specific endpoint
584
+ const api = {
585
+ posts: {
586
+ create: rpc.middleware(authMiddleware).command(async ({ title }) => {
587
+ // ...
588
+ }),
589
+ // Combine middleware with zod validation
590
+ update: rpc.middleware(authMiddleware).zod({
591
+ id: z.number(),
592
+ title: z.string(),
593
+ }).command(async ({ id, title }) => {
594
+ // ...
595
+ }),
596
+ },
597
+ };
598
+ ```
599
+
600
+ You can also attach middleware to any existing object with `.on()`:
601
+
602
+ ```typescript
603
+ const postsApi = {
604
+ list: rpc.query(async () => { ... }),
605
+ create: rpc.command(async () => { ... }),
606
+ };
607
+
608
+ // Attach authMiddleware to the whole postsApi group
609
+ rpc.middleware(authMiddleware).on(postsApi);
610
+
611
+ const api = { posts: postsApi };
612
+ ```