@srpc.org/core 0.20.3

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.
package/README.md ADDED
@@ -0,0 +1,322 @@
1
+ # @srpc/core
2
+
3
+ A lightweight, type-safe RPC framework for TypeScript with automatic type inference, multiple runtime support, and zero dependencies.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Using JSR (recommended)
9
+ deno add @srpc/core
10
+ npx jsr add @srpc/core
11
+ yarn dlx jsr add @srpc/core
12
+ pnpm dlx jsr add @srpc/core
13
+ bunx jsr add @srpc/core
14
+ ```
15
+
16
+ ## Features
17
+
18
+ - **Type-safe**: Full TypeScript support with automatic type inference
19
+ - **Zero dependencies**: Lightweight core with no external dependencies
20
+ - **Runtime agnostic**: Works with Node.js, Deno, Bun, Cloudflare Workers, and more
21
+ - **Multiple adapters**: Support for both Node.js HTTP and standard Fetch API
22
+ - **Nested routers**: Organize procedures into nested namespaces
23
+ - **Custom serialization**: Pluggable serialization layer
24
+ - **Error handling**: Built-in error types with HTTP status code mapping
25
+
26
+ ## Quick Start
27
+
28
+ ### Server Setup
29
+
30
+ #### Using Fetch API (Recommended for Edge Runtimes)
31
+
32
+ ```typescript
33
+ import { initSRPC, srpcFetchApi } from "@srpc/core/server";
34
+
35
+ // Initialize SRPC
36
+ const s = initSRPC();
37
+
38
+ // Define your router
39
+ const appRouter = s.router({
40
+ sayHello: async (_, name: string) => {
41
+ return `Hello ${name}!`;
42
+ },
43
+ getUser: async (_, id: number) => {
44
+ return { id, name: "John Doe", email: "john@example.com" };
45
+ },
46
+ });
47
+
48
+ // Export the router type for client usage
49
+ export type AppRouter = typeof appRouter;
50
+
51
+ // Create Fetch API handler
52
+ const { fetch: handleRequest } = srpcFetchApi({
53
+ router: appRouter,
54
+ endpoint: "/api",
55
+ });
56
+
57
+ // Use with any framework supporting Fetch API
58
+ export default {
59
+ fetch: handleRequest,
60
+ };
61
+ ```
62
+
63
+ #### Using Node.js HTTP Server
64
+
65
+ ```typescript
66
+ import { initSRPC } from "@srpc/core/server";
67
+ import { createSrpcServer } from "@srpc/core/server";
68
+
69
+ const s = initSRPC();
70
+
71
+ const appRouter = s.router({
72
+ sayHello: async (_, name: string) => {
73
+ return `Hello ${name}!`;
74
+ },
75
+ });
76
+
77
+ export type AppRouter = typeof appRouter;
78
+
79
+ // Create Node.js server
80
+ const server = createSrpcServer({
81
+ router: appRouter,
82
+ endpoint: "/api",
83
+ });
84
+
85
+ server.listen(3000, () => {
86
+ console.log("SRPC server listening on port 3000");
87
+ });
88
+ ```
89
+
90
+ ### Client Setup
91
+
92
+ ```typescript
93
+ import { createSRPCClient } from "@srpc/core/client";
94
+ import type { AppRouter } from "./server";
95
+
96
+ // Create type-safe client
97
+ const client = createSRPCClient<AppRouter>({
98
+ endpoint: "http://localhost:3000/api",
99
+ });
100
+
101
+ // Call procedures with full type safety
102
+ const greeting = await client.sayHello("World"); // Type: string
103
+ const user = await client.getUser(1); // Type: { id: number, name: string, email: string }
104
+ ```
105
+
106
+ ## Advanced Usage
107
+
108
+ ### Nested Routers
109
+
110
+ Organize your procedures into logical groups:
111
+
112
+ ```typescript
113
+ import { initSRPC } from "@srpc/core/server";
114
+
115
+ const s = initSRPC();
116
+
117
+ // Admin procedures
118
+ const adminRouter = s.router({
119
+ createUser: async (_, data: { name: string; email: string }) => {
120
+ // Create user logic
121
+ return { id: 1, ...data };
122
+ },
123
+ deleteUser: async (_, id: number) => {
124
+ // Delete user logic
125
+ return { success: true };
126
+ },
127
+ });
128
+
129
+ // Public user procedures
130
+ const usersRouter = s.router({
131
+ getUser: async (_, id: number) => {
132
+ return { id, name: "John Doe" };
133
+ },
134
+ admin: adminRouter, // Nested router
135
+ });
136
+
137
+ // Main app router
138
+ const appRouter = s.router({
139
+ users: usersRouter,
140
+ sayHello: async (_, name: string) => `Hello ${name}!`,
141
+ });
142
+
143
+ // Client usage:
144
+ // client.users.getUser(1)
145
+ // client.users.admin.createUser({ name: "Jane", email: "jane@example.com" })
146
+ // client.sayHello("World")
147
+ ```
148
+
149
+ ### Context
150
+
151
+ Add context (authentication, database connections, etc.) to your procedures:
152
+
153
+ ```typescript
154
+ import { initSRPC, srpcFetchApi } from "@srpc/core/server";
155
+
156
+ // Define your context type
157
+ type Context = {
158
+ user?: { id: number; name: string };
159
+ db: Database;
160
+ };
161
+
162
+ // Initialize with context type
163
+ const s = initSRPC().context<Context>();
164
+
165
+ // Use context in procedures
166
+ const appRouter = s.router({
167
+ getCurrentUser: async (ctx) => {
168
+ if (!ctx.user) throw new Error("Not authenticated");
169
+ return ctx.user;
170
+ },
171
+ getPost: async (ctx, id: number) => {
172
+ return await ctx.db.posts.findById(id);
173
+ },
174
+ });
175
+
176
+ // Provide context creation function
177
+ const { fetch: handleRequest } = srpcFetchApi({
178
+ router: appRouter,
179
+ endpoint: "/api",
180
+ createContext: async (req) => {
181
+ const user = await authenticateRequest(req);
182
+ return { user, db: getDatabase() };
183
+ },
184
+ });
185
+ ```
186
+
187
+ ### Error Handling
188
+
189
+ Use `SRPCError` for proper HTTP status code mapping:
190
+
191
+ ```typescript
192
+ import { initSRPC } from "@srpc/core/server";
193
+ import { SRPCError } from "@srpc.org/core";
194
+
195
+ const s = initSRPC();
196
+
197
+ const appRouter = s.router({
198
+ getUser: async (_, id: number) => {
199
+ const user = await db.users.findById(id);
200
+ if (!user) {
201
+ throw new SRPCError("User not found", "NOT_FOUND"); // Returns HTTP 404
202
+ }
203
+ return user;
204
+ },
205
+ deleteUser: async (_, id: number) => {
206
+ if (!hasPermission()) {
207
+ throw new SRPCError("Unauthorized", "UNAUTHORIZED"); // Returns HTTP 401
208
+ }
209
+ await db.users.delete(id);
210
+ return { success: true };
211
+ },
212
+ });
213
+
214
+ // Client-side error handling
215
+ try {
216
+ await client.getUser(999);
217
+ } catch (error) {
218
+ if (error instanceof SRPCError) {
219
+ console.log(error.code); // "NOT_FOUND"
220
+ console.log(error.message); // "User not found"
221
+ }
222
+ }
223
+ ```
224
+
225
+ ### Custom Serialization
226
+
227
+ Use custom serializers for complex data types:
228
+
229
+ ```typescript
230
+ import { createSRPCClient } from "@srpc/core/client";
231
+ import type { Serializer } from "@srpc/core/shared";
232
+
233
+ // Example: Using superjson for Date, Map, Set support
234
+ import superjson from "superjson";
235
+
236
+ const customSerializer: Serializer = {
237
+ serialize: (value) => superjson.stringify(value),
238
+ deserialize: (value) => superjson.parse(value),
239
+ };
240
+
241
+ const client = createSRPCClient<AppRouter>({
242
+ endpoint: "http://localhost:3000/api",
243
+ transformer: customSerializer,
244
+ });
245
+
246
+ // Server-side
247
+ const { fetch: handleRequest } = srpcFetchApi({
248
+ router: appRouter,
249
+ endpoint: "/api",
250
+ transformer: customSerializer,
251
+ });
252
+ ```
253
+
254
+ ### Type Inference Utilities
255
+
256
+ Extract input and output types from your router:
257
+
258
+ ```typescript
259
+ import type { InferRouterInputs, InferRouterOutputs } from "@srpc.org/core";
260
+ import type { AppRouter } from "./server";
261
+
262
+ // Get all input types
263
+ type RouterInputs = InferRouterInputs<AppRouter>;
264
+ type GetUserInput = RouterInputs["getUser"]; // [id: number]
265
+
266
+ // Get all output types
267
+ type RouterOutputs = InferRouterOutputs<AppRouter>;
268
+ type GetUserOutput = RouterOutputs["getUser"]; // { id: number, name: string, email: string }
269
+ ```
270
+
271
+ ### Server-Side Calling
272
+
273
+ Call procedures directly on the server without HTTP:
274
+
275
+ ```typescript
276
+ import { createSRPCCaller } from "@srpc/core/server";
277
+ import { appRouter } from "./router";
278
+
279
+ const caller = createSRPCCaller({
280
+ router: appRouter,
281
+ createContext: async () => ({ user: null, db: getDatabase() }),
282
+ });
283
+
284
+ // Call procedures directly
285
+ const result = await caller.sayHello("World");
286
+ ```
287
+
288
+ ## API Reference
289
+
290
+ ### Server
291
+
292
+ - `initSRPC()` - Initialize SRPC builder
293
+ - `srpcFetchApi(options)` - Create Fetch API handler
294
+ - `createSrpcServer(options)` - Create Node.js HTTP server
295
+ - `createSRPCCaller(options)` - Create server-side caller
296
+
297
+ ### Client
298
+
299
+ - `createSRPCClient<TRouter>(options)` - Create type-safe client
300
+
301
+ ### Shared
302
+
303
+ - `SRPCError` - Error class with HTTP status mapping
304
+ - `ErrorCodes` - Available error codes
305
+ - `Serializer` - Serialization interface
306
+ - `InferRouterInputs<TRouter>` - Extract input types
307
+ - `InferRouterOutputs<TRouter>` - Extract output types
308
+
309
+ ## Error Codes
310
+
311
+ - `BAD_REQUEST` → HTTP 400
312
+ - `UNAUTHORIZED` → HTTP 401
313
+ - `FORBIDDEN` → HTTP 403
314
+ - `NOT_FOUND` → HTTP 404
315
+ - `UNSUPPORTED_MEDIA_TYPE` → HTTP 415
316
+ - `INTERNAL_SERVER_ERROR` → HTTP 500
317
+ - `NOT_IMPLEMENTED` → HTTP 501
318
+ - `GENERIC_ERROR` → HTTP 500
319
+
320
+ ## License
321
+
322
+ MIT
@@ -0,0 +1,149 @@
1
+ import { AnySRPC } from './server.js';
2
+ import { Serializer, DecoratedProcedureRecord } from './shared.js';
3
+ export * from './shared.js';
4
+
5
+ type ProcedureType<TContext> = (
6
+ ctx: TContext,
7
+ ...args: any[]
8
+ ) => Promise<any> | any;
9
+
10
+ type Routes<TContext> = {
11
+ [key: string]: ProcedureType<TContext> | SRPC<TContext>;
12
+ };
13
+ declare class SRPC<TContext, TRoutes extends Routes<TContext> = {}> {
14
+ __context: TContext;
15
+ ipc: TRoutes;
16
+ constructor(routes?: TRoutes);
17
+ context<T>(): SRPC<T>;
18
+ router<T extends Routes<TContext>>(routes: T): SRPC<TContext, T>;
19
+ }
20
+
21
+ /**
22
+ * @module
23
+ *
24
+ * Client-side SRPC module for creating type-safe RPC clients.
25
+ *
26
+ * This module provides the `createSRPCClient` function which creates a proxy-based
27
+ * client that converts method calls into HTTP requests to your SRPC server.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { createSRPCClient } from "@srpc/core/client";
32
+ * import type { AppRouter } from "./server";
33
+ *
34
+ * const client = createSRPCClient<AppRouter>({
35
+ * endpoint: "http://localhost:3000/api",
36
+ * headers: async () => ({
37
+ * "Authorization": `Bearer ${await getToken()}`
38
+ * })
39
+ * });
40
+ *
41
+ * // Type-safe procedure calls
42
+ * const user = await client.users.getUser(1);
43
+ * const result = await client.users.admin.createUser({ name: "Jane" });
44
+ * ```
45
+ */
46
+
47
+ /**
48
+ * Configuration options for creating an SRPC client.
49
+ *
50
+ * @property endpoint - Base URL for the SRPC server (e.g., "http://localhost:3000/api")
51
+ * @property headers - Optional function to provide custom headers for each request
52
+ * @property transformer - Optional custom serializer for request/response data (default: JSON)
53
+ * @property fetch - Optional custom fetch implementation (default: globalThis.fetch)
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * const options: SRPCClientOptions = {
58
+ * endpoint: "http://localhost:3000/api",
59
+ * headers: async ({ path }) => {
60
+ * const token = await getAuthToken();
61
+ * return {
62
+ * "Authorization": `Bearer ${token}`,
63
+ * "X-Request-Path": path.join(".")
64
+ * };
65
+ * },
66
+ * transformer: customSerializer,
67
+ * fetch: customFetch
68
+ * };
69
+ * ```
70
+ */
71
+ type SRPCClientOptions = {
72
+ endpoint: string;
73
+ headers?: (op: {
74
+ path: readonly string[];
75
+ }) => HeadersInit | Promise<HeadersInit>;
76
+ transformer?: Serializer;
77
+ fetch?: typeof fetch;
78
+ };
79
+ /**
80
+ * Creates a type-safe SRPC client that converts method calls into HTTP requests.
81
+ *
82
+ * The client uses JavaScript Proxies to intercept property access and method calls,
83
+ * automatically converting them into HTTP POST requests to the server. All procedure
84
+ * calls are fully type-safe based on the router type provided.
85
+ *
86
+ * @template TRouter - The SRPC router type from your server
87
+ * @template TRoutes - Internal routes type (inferred automatically)
88
+ *
89
+ * @param options - Client configuration options
90
+ * @returns A type-safe client proxy matching your server's router structure
91
+ *
92
+ * @example Basic usage
93
+ * ```typescript
94
+ * import { createSRPCClient } from "@srpc/core/client";
95
+ * import type { AppRouter } from "./server";
96
+ *
97
+ * const client = createSRPCClient<AppRouter>({
98
+ * endpoint: "http://localhost:3000/api"
99
+ * });
100
+ *
101
+ * // All calls are type-checked
102
+ * const greeting = await client.sayHello("World"); // string
103
+ * const user = await client.users.getUser(1); // User type
104
+ * ```
105
+ *
106
+ * @example With authentication headers
107
+ * ```typescript
108
+ * const client = createSRPCClient<AppRouter>({
109
+ * endpoint: "http://localhost:3000/api",
110
+ * headers: async () => ({
111
+ * "Authorization": `Bearer ${await getAuthToken()}`
112
+ * })
113
+ * });
114
+ * ```
115
+ *
116
+ * @example With custom serialization
117
+ * ```typescript
118
+ * import superjson from "superjson";
119
+ *
120
+ * const client = createSRPCClient<AppRouter>({
121
+ * endpoint: "http://localhost:3000/api",
122
+ * transformer: {
123
+ * serialize: (value) => superjson.stringify(value),
124
+ * deserialize: (value) => superjson.parse(value)
125
+ * }
126
+ * });
127
+ *
128
+ * // Now Date, Map, Set, etc. are properly serialized
129
+ * await client.createEvent({ date: new Date() });
130
+ * ```
131
+ *
132
+ * @example Error handling
133
+ * ```typescript
134
+ * import { SRPCError } from "@srpc.org/core";
135
+ *
136
+ * try {
137
+ * await client.users.getUser(999);
138
+ * } catch (error) {
139
+ * if (error instanceof SRPCError) {
140
+ * console.log(error.code); // "NOT_FOUND"
141
+ * console.log(error.message); // "User not found"
142
+ * }
143
+ * }
144
+ * ```
145
+ */
146
+ declare const createSRPCClient: <TRouter extends AnySRPC, TRoutes extends Routes<any> = TRouter["ipc"]>({ endpoint, headers: getHeaders, transformer, fetch, }: SRPCClientOptions) => DecoratedProcedureRecord<TRoutes>;
147
+
148
+ export { createSRPCClient };
149
+ export type { SRPCClientOptions };
package/dist/client.js ADDED
@@ -0,0 +1,158 @@
1
+ import { defaultSerializer, SRPCError } from './shared.js';
2
+ export * from './shared.js';
3
+
4
+ /**
5
+ * Used from trpc.io
6
+ */ const noop = ()=>{
7
+ // noop
8
+ };
9
+ const freezeIfAvailable = (obj)=>{
10
+ if (Object.freeze) {
11
+ Object.freeze(obj);
12
+ }
13
+ };
14
+ function createInnerProxy(callback, path, memo) {
15
+ var _memo, _cacheKey;
16
+ const cacheKey = path.join(".");
17
+ (_memo = memo)[_cacheKey = cacheKey] ?? (_memo[_cacheKey] = new Proxy(noop, {
18
+ get (_obj, key) {
19
+ if (typeof key !== "string" || key === "then") {
20
+ // special case for if the proxy is accidentally treated
21
+ // like a PromiseLike (like in `Promise.resolve(proxy)`)
22
+ return undefined;
23
+ }
24
+ return createInnerProxy(callback, [
25
+ ...path,
26
+ key
27
+ ], memo);
28
+ },
29
+ apply (_1, _2, args) {
30
+ const lastOfPath = path[path.length - 1];
31
+ let opts = {
32
+ args,
33
+ path
34
+ };
35
+ // special handling for e.g. `trpc.hello.call(this, 'there')` and `trpc.hello.apply(this, ['there'])
36
+ if (lastOfPath === "call") {
37
+ opts = {
38
+ args: args.length >= 2 ? [
39
+ args[1]
40
+ ] : [],
41
+ path: path.slice(0, -1)
42
+ };
43
+ } else if (lastOfPath === "apply") {
44
+ opts = {
45
+ args: args.length >= 2 ? args[1] : [],
46
+ path: path.slice(0, -1)
47
+ };
48
+ } else if (lastOfPath === "toString") {
49
+ return path.slice(0, -1).join(".");
50
+ } else if (lastOfPath === "toJSON") {
51
+ return {
52
+ path: path.slice(0, -1),
53
+ pathString: path.slice(0, -1).join("."),
54
+ __type: "SRPC"
55
+ };
56
+ }
57
+ freezeIfAvailable(opts.args);
58
+ freezeIfAvailable(opts.path);
59
+ return callback(opts);
60
+ }
61
+ }));
62
+ return memo[cacheKey];
63
+ }
64
+ /**
65
+ * Creates a proxy that calls the callback with the path and arguments
66
+ *
67
+ * @internal
68
+ */ const createRecursiveProxy = (callback)=>createInnerProxy(callback, [], Object.create(null));
69
+
70
+ /**
71
+ * Creates a type-safe SRPC client that converts method calls into HTTP requests.
72
+ *
73
+ * The client uses JavaScript Proxies to intercept property access and method calls,
74
+ * automatically converting them into HTTP POST requests to the server. All procedure
75
+ * calls are fully type-safe based on the router type provided.
76
+ *
77
+ * @template TRouter - The SRPC router type from your server
78
+ * @template TRoutes - Internal routes type (inferred automatically)
79
+ *
80
+ * @param options - Client configuration options
81
+ * @returns A type-safe client proxy matching your server's router structure
82
+ *
83
+ * @example Basic usage
84
+ * ```typescript
85
+ * import { createSRPCClient } from "@srpc/core/client";
86
+ * import type { AppRouter } from "./server";
87
+ *
88
+ * const client = createSRPCClient<AppRouter>({
89
+ * endpoint: "http://localhost:3000/api"
90
+ * });
91
+ *
92
+ * // All calls are type-checked
93
+ * const greeting = await client.sayHello("World"); // string
94
+ * const user = await client.users.getUser(1); // User type
95
+ * ```
96
+ *
97
+ * @example With authentication headers
98
+ * ```typescript
99
+ * const client = createSRPCClient<AppRouter>({
100
+ * endpoint: "http://localhost:3000/api",
101
+ * headers: async () => ({
102
+ * "Authorization": `Bearer ${await getAuthToken()}`
103
+ * })
104
+ * });
105
+ * ```
106
+ *
107
+ * @example With custom serialization
108
+ * ```typescript
109
+ * import superjson from "superjson";
110
+ *
111
+ * const client = createSRPCClient<AppRouter>({
112
+ * endpoint: "http://localhost:3000/api",
113
+ * transformer: {
114
+ * serialize: (value) => superjson.stringify(value),
115
+ * deserialize: (value) => superjson.parse(value)
116
+ * }
117
+ * });
118
+ *
119
+ * // Now Date, Map, Set, etc. are properly serialized
120
+ * await client.createEvent({ date: new Date() });
121
+ * ```
122
+ *
123
+ * @example Error handling
124
+ * ```typescript
125
+ * import { SRPCError } from "@srpc.org/core";
126
+ *
127
+ * try {
128
+ * await client.users.getUser(999);
129
+ * } catch (error) {
130
+ * if (error instanceof SRPCError) {
131
+ * console.log(error.code); // "NOT_FOUND"
132
+ * console.log(error.message); // "User not found"
133
+ * }
134
+ * }
135
+ * ```
136
+ */ const createSRPCClient = ({ endpoint, headers: getHeaders, transformer = defaultSerializer, fetch = globalThis.fetch })=>{
137
+ return createRecursiveProxy(async ({ path, args })=>{
138
+ const headers = await getHeaders?.({
139
+ path: path
140
+ });
141
+ const response = await fetch(`${endpoint}/${path.join(".")}`, {
142
+ method: "POST",
143
+ body: transformer.serialize(args),
144
+ headers
145
+ });
146
+ const responseText = await response.text();
147
+ const data = transformer.deserialize(responseText);
148
+ if (!response.ok) {
149
+ if ("__BRAND__" in data && data.__BRAND__ === "SRPCError") {
150
+ throw new SRPCError(data.message, data.code);
151
+ }
152
+ throw new SRPCError(response.statusText || "Unknown error", "GENERIC_ERROR");
153
+ }
154
+ return data;
155
+ });
156
+ };
157
+
158
+ export { createSRPCClient };
@@ -0,0 +1 @@
1
+ export { ErrorCodes, InferRouterInputs, InferRouterOutputs, SRPCError } from './shared.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { SRPCError } from './shared.js';