@richie-rpc/client 0.1.0

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,193 @@
1
+ # @richie-rpc/client
2
+
3
+ Type-safe fetch client for Richie RPC contracts.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @richie-rpc/client @richie-rpc/core zod
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Creating a Client
14
+
15
+ ```typescript
16
+ import { createClient } from '@richie-rpc/client';
17
+ import { contract } from './contract';
18
+
19
+ const client = createClient(contract, {
20
+ baseUrl: 'https://api.example.com',
21
+ headers: {
22
+ 'Authorization': 'Bearer token123'
23
+ }
24
+ });
25
+ ```
26
+
27
+ ### Making Requests
28
+
29
+ The client provides fully typed methods for each endpoint in your contract:
30
+
31
+ ```typescript
32
+ // GET request with path parameters
33
+ const user = await client.getUser({
34
+ params: { id: '123' }
35
+ });
36
+ // user is typed based on the response schema
37
+
38
+ // POST request with body
39
+ const newUser = await client.createUser({
40
+ body: {
41
+ name: 'John Doe',
42
+ email: 'john@example.com'
43
+ }
44
+ });
45
+
46
+ // Request with query parameters
47
+ const users = await client.listUsers({
48
+ query: {
49
+ limit: '10',
50
+ offset: '0'
51
+ }
52
+ });
53
+
54
+ // Request with custom headers
55
+ const data = await client.getData({
56
+ headers: {
57
+ 'X-Custom-Header': 'value'
58
+ }
59
+ });
60
+ ```
61
+
62
+ ## Features
63
+
64
+ - ✅ Full type safety based on contract
65
+ - ✅ Automatic path parameter interpolation
66
+ - ✅ Query parameter encoding
67
+ - ✅ Request validation before sending
68
+ - ✅ Response validation after receiving
69
+ - ✅ Detailed error information
70
+ - ✅ Support for all HTTP methods
71
+ - ✅ Custom headers per request
72
+
73
+ ## Configuration
74
+
75
+ ### ClientConfig Options
76
+
77
+ ```typescript
78
+ interface ClientConfig {
79
+ baseUrl: string; // Base URL for all requests
80
+ headers?: Record<string, string>; // Default headers
81
+ validateRequest?: boolean; // Validate before sending (default: true)
82
+ validateResponse?: boolean; // Validate after receiving (default: true)
83
+ }
84
+ ```
85
+
86
+ ## Response Format
87
+
88
+ Responses include both the status code and data:
89
+
90
+ ```typescript
91
+ const response = await client.getUser({ params: { id: '123' } });
92
+
93
+ console.log(response.status); // 200, 404, etc.
94
+ console.log(response.data); // Typed response body
95
+ ```
96
+
97
+ ## Error Handling
98
+
99
+ The client throws typed errors for different scenarios:
100
+
101
+ ### ClientValidationError
102
+
103
+ Thrown when request data fails validation:
104
+
105
+ ```typescript
106
+ try {
107
+ await client.createUser({
108
+ body: { email: 'invalid-email' }
109
+ });
110
+ } catch (error) {
111
+ if (error instanceof ClientValidationError) {
112
+ console.log(error.field); // 'body'
113
+ console.log(error.issues); // Zod validation issues
114
+ }
115
+ }
116
+ ```
117
+
118
+ ### HTTPError
119
+
120
+ Thrown for unexpected HTTP status codes:
121
+
122
+ ```typescript
123
+ try {
124
+ await client.getUser({ params: { id: '999' } });
125
+ } catch (error) {
126
+ if (error instanceof HTTPError) {
127
+ console.log(error.status); // 404
128
+ console.log(error.statusText); // 'Not Found'
129
+ console.log(error.body); // Response body
130
+ }
131
+ }
132
+ ```
133
+
134
+ ## Type Safety
135
+
136
+ All client methods are fully typed based on your contract:
137
+
138
+ ```typescript
139
+ // ✅ Type-safe: required fields
140
+ await client.createUser({
141
+ body: { name: 'John', email: 'john@example.com' }
142
+ });
143
+
144
+ // ❌ Type error: missing required field
145
+ await client.createUser({
146
+ body: { name: 'John' }
147
+ });
148
+
149
+ // ✅ Type-safe: response data
150
+ const user = await client.getUser({ params: { id: '123' } });
151
+ console.log(user.data.name); // string
152
+
153
+ // ❌ Type error: invalid property
154
+ console.log(user.data.invalid);
155
+ ```
156
+
157
+ ## Request Options
158
+
159
+ Each client method accepts an options object with the following fields (based on the endpoint definition):
160
+
161
+ - `params`: Path parameters (if endpoint has params schema)
162
+ - `query`: Query parameters (if endpoint has query schema)
163
+ - `headers`: Custom headers (if endpoint has headers schema)
164
+ - `body`: Request body (if endpoint has body schema)
165
+
166
+ Only the fields defined in the contract are available and typed.
167
+
168
+ ## Validation
169
+
170
+ By default, both request and response data are validated:
171
+
172
+ - **Request validation**: Ensures data conforms to schema before sending
173
+ - **Response validation**: Ensures server response matches expected schema
174
+
175
+ You can disable validation:
176
+
177
+ ```typescript
178
+ const client = createClient(contract, {
179
+ baseUrl: 'https://api.example.com',
180
+ validateRequest: false, // Skip request validation
181
+ validateResponse: false // Skip response validation
182
+ });
183
+ ```
184
+
185
+ ## Links
186
+
187
+ - **npm:** https://www.npmjs.com/package/@richie-rpc/client
188
+ - **Repository:** https://github.com/ricsam/richie-rpc
189
+
190
+ ## License
191
+
192
+ MIT
193
+
@@ -0,0 +1,172 @@
1
+ // @bun @bun-cjs
2
+ (function(exports, require, module, __filename, __dirname) {var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
7
+ var __toCommonJS = (from) => {
8
+ var entry = __moduleCache.get(from), desc;
9
+ if (entry)
10
+ return entry;
11
+ entry = __defProp({}, "__esModule", { value: true });
12
+ if (from && typeof from === "object" || typeof from === "function")
13
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ }));
17
+ __moduleCache.set(from, entry);
18
+ return entry;
19
+ };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true,
25
+ configurable: true,
26
+ set: (newValue) => all[name] = () => newValue
27
+ });
28
+ };
29
+
30
+ // packages/client/index.ts
31
+ var exports_client = {};
32
+ __export(exports_client, {
33
+ createTypedClient: () => createTypedClient,
34
+ createClient: () => createClient,
35
+ HTTPError: () => HTTPError,
36
+ ClientValidationError: () => ClientValidationError
37
+ });
38
+ module.exports = __toCommonJS(exports_client);
39
+ var import_core = require("@richie-rpc/core");
40
+
41
+ class ClientValidationError extends Error {
42
+ field;
43
+ issues;
44
+ constructor(field, issues) {
45
+ super(`Validation failed for ${field}`);
46
+ this.field = field;
47
+ this.issues = issues;
48
+ this.name = "ClientValidationError";
49
+ }
50
+ }
51
+
52
+ class HTTPError extends Error {
53
+ status;
54
+ statusText;
55
+ body;
56
+ constructor(status, statusText, body) {
57
+ super(`HTTP Error ${status}: ${statusText}`);
58
+ this.status = status;
59
+ this.statusText = statusText;
60
+ this.body = body;
61
+ this.name = "HTTPError";
62
+ }
63
+ }
64
+ function validateRequest(endpoint, options) {
65
+ if (endpoint.params && options.params) {
66
+ const result = endpoint.params.safeParse(options.params);
67
+ if (!result.success) {
68
+ throw new ClientValidationError("params", result.error.issues);
69
+ }
70
+ }
71
+ if (endpoint.query && options.query) {
72
+ const result = endpoint.query.safeParse(options.query);
73
+ if (!result.success) {
74
+ throw new ClientValidationError("query", result.error.issues);
75
+ }
76
+ }
77
+ if (endpoint.headers && options.headers) {
78
+ const result = endpoint.headers.safeParse(options.headers);
79
+ if (!result.success) {
80
+ throw new ClientValidationError("headers", result.error.issues);
81
+ }
82
+ }
83
+ if (endpoint.body && options.body) {
84
+ const result = endpoint.body.safeParse(options.body);
85
+ if (!result.success) {
86
+ throw new ClientValidationError("body", result.error.issues);
87
+ }
88
+ }
89
+ }
90
+ function validateResponse(endpoint, status, data) {
91
+ const responseSchema = endpoint.responses[status];
92
+ if (responseSchema) {
93
+ const result = responseSchema.safeParse(data);
94
+ if (!result.success) {
95
+ throw new ClientValidationError(`response[${status}]`, result.error.issues);
96
+ }
97
+ }
98
+ }
99
+ async function makeRequest(config, endpoint, options) {
100
+ if (config.validateRequest !== false) {
101
+ validateRequest(endpoint, options);
102
+ }
103
+ let path = endpoint.path;
104
+ if (options.params) {
105
+ path = import_core.interpolatePath(path, options.params);
106
+ }
107
+ const url = import_core.buildUrl(config.baseUrl, path, options.query);
108
+ const headers = new Headers(config.headers);
109
+ if (options.headers) {
110
+ for (const [key, value] of Object.entries(options.headers)) {
111
+ headers.set(key, String(value));
112
+ }
113
+ }
114
+ const init = {
115
+ method: endpoint.method,
116
+ headers
117
+ };
118
+ if (options.body !== undefined) {
119
+ headers.set("content-type", "application/json");
120
+ init.body = JSON.stringify(options.body);
121
+ }
122
+ const response = await fetch(url, init);
123
+ let data;
124
+ if (response.status === 204) {
125
+ data = {};
126
+ } else {
127
+ const contentType = response.headers.get("content-type") || "";
128
+ if (contentType.includes("application/json")) {
129
+ data = await response.json();
130
+ } else if (contentType.includes("text/")) {
131
+ data = await response.text();
132
+ } else {
133
+ const text = await response.text();
134
+ if (text) {
135
+ data = text;
136
+ } else {
137
+ data = {};
138
+ }
139
+ }
140
+ }
141
+ if (!response.ok && !(response.status in endpoint.responses)) {
142
+ throw new HTTPError(response.status, response.statusText, data);
143
+ }
144
+ if (config.validateResponse !== false) {
145
+ validateResponse(endpoint, response.status, data);
146
+ }
147
+ return {
148
+ status: response.status,
149
+ data
150
+ };
151
+ }
152
+ function createClient(contract, config) {
153
+ const client = {};
154
+ for (const [name, endpoint] of Object.entries(contract)) {
155
+ client[name] = (options = {}) => {
156
+ return makeRequest(config, endpoint, options);
157
+ };
158
+ }
159
+ return client;
160
+ }
161
+ function createTypedClient(_config) {
162
+ return new Proxy({}, {
163
+ get(_target, _prop) {
164
+ return async (_options = {}) => {
165
+ throw new Error("createTypedClient requires contract at runtime for validation. Use createClient instead.");
166
+ };
167
+ }
168
+ });
169
+ }
170
+ })
171
+
172
+ //# debugId=BC1C6659CB2717F064756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../index.ts"],
4
+ "sourcesContent": [
5
+ "import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Client configuration\nexport interface ClientConfig {\n baseUrl: string;\n headers?: Record<string, string>;\n validateRequest?: boolean;\n validateResponse?: boolean;\n}\n\n// Request options for an endpoint\nexport type EndpointRequestOptions<T extends EndpointDefinition> = {\n params?: ExtractParams<T> extends never ? never : ExtractParams<T>;\n query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;\n headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;\n body?: ExtractBody<T> extends never ? never : ExtractBody<T>;\n};\n\n// Response type for an endpoint (union of all possible responses)\nexport type EndpointResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n };\n}[keyof T['responses']];\n\n// Client method type for an endpoint\nexport type ClientMethod<T extends EndpointDefinition> = (\n options: EndpointRequestOptions<T>,\n) => Promise<EndpointResponse<T>>;\n\n// Client type for a contract\nexport type Client<T extends Contract> = {\n [K in keyof T]: ClientMethod<T[K]>;\n};\n\n// Validation error\nexport class ClientValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n ) {\n super(`Validation failed for ${field}`);\n this.name = 'ClientValidationError';\n }\n}\n\n// HTTP error\nexport class HTTPError extends Error {\n constructor(\n public status: number,\n public statusText: string,\n public body: unknown,\n ) {\n super(`HTTP Error ${status}: ${statusText}`);\n this.name = 'HTTPError';\n }\n}\n\n/**\n * Validate request data before sending\n */\nfunction validateRequest<T extends EndpointDefinition>(\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): void {\n // Validate params\n if (endpoint.params && options.params) {\n const result = endpoint.params.safeParse(options.params);\n if (!result.success) {\n throw new ClientValidationError('params', result.error.issues);\n }\n }\n\n // Validate query\n if (endpoint.query && options.query) {\n const result = endpoint.query.safeParse(options.query);\n if (!result.success) {\n throw new ClientValidationError('query', result.error.issues);\n }\n }\n\n // Validate headers\n if (endpoint.headers && options.headers) {\n const result = endpoint.headers.safeParse(options.headers);\n if (!result.success) {\n throw new ClientValidationError('headers', result.error.issues);\n }\n }\n\n // Validate body\n if (endpoint.body && options.body) {\n const result = endpoint.body.safeParse(options.body);\n if (!result.success) {\n throw new ClientValidationError('body', result.error.issues);\n }\n }\n}\n\n/**\n * Validate response data after receiving\n */\nfunction validateResponse<T extends EndpointDefinition>(\n endpoint: T,\n status: number,\n data: unknown,\n): void {\n const responseSchema = endpoint.responses[status];\n if (responseSchema) {\n const result = responseSchema.safeParse(data);\n if (!result.success) {\n throw new ClientValidationError(`response[${status}]`, result.error.issues);\n }\n }\n}\n\n/**\n * Make a request to an endpoint\n */\nasync function makeRequest<T extends EndpointDefinition>(\n config: ClientConfig,\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): Promise<EndpointResponse<T>> {\n // Validate request if enabled\n if (config.validateRequest !== false) {\n validateRequest(endpoint, options);\n }\n\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n config.baseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n // Build headers\n const headers = new Headers(config.headers);\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.set(key, String(value));\n }\n }\n\n // Build request init\n const init: RequestInit = {\n method: endpoint.method,\n headers,\n };\n\n // Add body if present\n if (options.body !== undefined) {\n headers.set('content-type', 'application/json');\n init.body = JSON.stringify(options.body);\n }\n\n // Make request\n const response = await fetch(url, init);\n\n // Parse response\n let data: unknown;\n\n // Handle 204 No Content\n if (response.status === 204) {\n data = {};\n } else {\n const contentType = response.headers.get('content-type') || '';\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else if (contentType.includes('text/')) {\n data = await response.text();\n } else {\n // Check if there's any content\n const text = await response.text();\n if (text) {\n data = text;\n } else {\n data = {};\n }\n }\n }\n\n // Check for HTTP errors\n if (!response.ok && !(response.status in endpoint.responses)) {\n throw new HTTPError(response.status, response.statusText, data);\n }\n\n // Validate response if enabled\n if (config.validateResponse !== false) {\n validateResponse(endpoint, response.status, data);\n }\n\n return {\n status: response.status,\n data,\n } as EndpointResponse<T>;\n}\n\n/**\n * Create a typesafe client for a contract\n */\nexport function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T> {\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n return makeRequest(config, endpoint, options);\n };\n }\n\n return client as Client<T>;\n}\n\n/**\n * Create a client without providing the contract at runtime\n * Useful when you only need types and want a lighter bundle\n */\nexport function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T> {\n return new Proxy({} as Client<T>, {\n get(_target, _prop: string) {\n return async (_options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n // Without the contract, we can't validate or infer the endpoint\n // This is just a basic fetch wrapper with typing\n throw new Error(\n 'createTypedClient requires contract at runtime for validation. Use createClient instead.',\n );\n };\n },\n });\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "BC1C6659CB2717F064756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@richie-rpc/client",
3
+ "version": "0.1.0",
4
+ "type": "commonjs"
5
+ }
@@ -0,0 +1,141 @@
1
+ // @bun
2
+ // packages/client/index.ts
3
+ import { buildUrl, interpolatePath } from "@richie-rpc/core";
4
+
5
+ class ClientValidationError extends Error {
6
+ field;
7
+ issues;
8
+ constructor(field, issues) {
9
+ super(`Validation failed for ${field}`);
10
+ this.field = field;
11
+ this.issues = issues;
12
+ this.name = "ClientValidationError";
13
+ }
14
+ }
15
+
16
+ class HTTPError extends Error {
17
+ status;
18
+ statusText;
19
+ body;
20
+ constructor(status, statusText, body) {
21
+ super(`HTTP Error ${status}: ${statusText}`);
22
+ this.status = status;
23
+ this.statusText = statusText;
24
+ this.body = body;
25
+ this.name = "HTTPError";
26
+ }
27
+ }
28
+ function validateRequest(endpoint, options) {
29
+ if (endpoint.params && options.params) {
30
+ const result = endpoint.params.safeParse(options.params);
31
+ if (!result.success) {
32
+ throw new ClientValidationError("params", result.error.issues);
33
+ }
34
+ }
35
+ if (endpoint.query && options.query) {
36
+ const result = endpoint.query.safeParse(options.query);
37
+ if (!result.success) {
38
+ throw new ClientValidationError("query", result.error.issues);
39
+ }
40
+ }
41
+ if (endpoint.headers && options.headers) {
42
+ const result = endpoint.headers.safeParse(options.headers);
43
+ if (!result.success) {
44
+ throw new ClientValidationError("headers", result.error.issues);
45
+ }
46
+ }
47
+ if (endpoint.body && options.body) {
48
+ const result = endpoint.body.safeParse(options.body);
49
+ if (!result.success) {
50
+ throw new ClientValidationError("body", result.error.issues);
51
+ }
52
+ }
53
+ }
54
+ function validateResponse(endpoint, status, data) {
55
+ const responseSchema = endpoint.responses[status];
56
+ if (responseSchema) {
57
+ const result = responseSchema.safeParse(data);
58
+ if (!result.success) {
59
+ throw new ClientValidationError(`response[${status}]`, result.error.issues);
60
+ }
61
+ }
62
+ }
63
+ async function makeRequest(config, endpoint, options) {
64
+ if (config.validateRequest !== false) {
65
+ validateRequest(endpoint, options);
66
+ }
67
+ let path = endpoint.path;
68
+ if (options.params) {
69
+ path = interpolatePath(path, options.params);
70
+ }
71
+ const url = buildUrl(config.baseUrl, path, options.query);
72
+ const headers = new Headers(config.headers);
73
+ if (options.headers) {
74
+ for (const [key, value] of Object.entries(options.headers)) {
75
+ headers.set(key, String(value));
76
+ }
77
+ }
78
+ const init = {
79
+ method: endpoint.method,
80
+ headers
81
+ };
82
+ if (options.body !== undefined) {
83
+ headers.set("content-type", "application/json");
84
+ init.body = JSON.stringify(options.body);
85
+ }
86
+ const response = await fetch(url, init);
87
+ let data;
88
+ if (response.status === 204) {
89
+ data = {};
90
+ } else {
91
+ const contentType = response.headers.get("content-type") || "";
92
+ if (contentType.includes("application/json")) {
93
+ data = await response.json();
94
+ } else if (contentType.includes("text/")) {
95
+ data = await response.text();
96
+ } else {
97
+ const text = await response.text();
98
+ if (text) {
99
+ data = text;
100
+ } else {
101
+ data = {};
102
+ }
103
+ }
104
+ }
105
+ if (!response.ok && !(response.status in endpoint.responses)) {
106
+ throw new HTTPError(response.status, response.statusText, data);
107
+ }
108
+ if (config.validateResponse !== false) {
109
+ validateResponse(endpoint, response.status, data);
110
+ }
111
+ return {
112
+ status: response.status,
113
+ data
114
+ };
115
+ }
116
+ function createClient(contract, config) {
117
+ const client = {};
118
+ for (const [name, endpoint] of Object.entries(contract)) {
119
+ client[name] = (options = {}) => {
120
+ return makeRequest(config, endpoint, options);
121
+ };
122
+ }
123
+ return client;
124
+ }
125
+ function createTypedClient(_config) {
126
+ return new Proxy({}, {
127
+ get(_target, _prop) {
128
+ return async (_options = {}) => {
129
+ throw new Error("createTypedClient requires contract at runtime for validation. Use createClient instead.");
130
+ };
131
+ }
132
+ });
133
+ }
134
+ export {
135
+ createTypedClient,
136
+ createClient,
137
+ HTTPError,
138
+ ClientValidationError
139
+ };
140
+
141
+ //# debugId=1B7C82F232BA1F0964756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../index.ts"],
4
+ "sourcesContent": [
5
+ "import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Client configuration\nexport interface ClientConfig {\n baseUrl: string;\n headers?: Record<string, string>;\n validateRequest?: boolean;\n validateResponse?: boolean;\n}\n\n// Request options for an endpoint\nexport type EndpointRequestOptions<T extends EndpointDefinition> = {\n params?: ExtractParams<T> extends never ? never : ExtractParams<T>;\n query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;\n headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;\n body?: ExtractBody<T> extends never ? never : ExtractBody<T>;\n};\n\n// Response type for an endpoint (union of all possible responses)\nexport type EndpointResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n };\n}[keyof T['responses']];\n\n// Client method type for an endpoint\nexport type ClientMethod<T extends EndpointDefinition> = (\n options: EndpointRequestOptions<T>,\n) => Promise<EndpointResponse<T>>;\n\n// Client type for a contract\nexport type Client<T extends Contract> = {\n [K in keyof T]: ClientMethod<T[K]>;\n};\n\n// Validation error\nexport class ClientValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n ) {\n super(`Validation failed for ${field}`);\n this.name = 'ClientValidationError';\n }\n}\n\n// HTTP error\nexport class HTTPError extends Error {\n constructor(\n public status: number,\n public statusText: string,\n public body: unknown,\n ) {\n super(`HTTP Error ${status}: ${statusText}`);\n this.name = 'HTTPError';\n }\n}\n\n/**\n * Validate request data before sending\n */\nfunction validateRequest<T extends EndpointDefinition>(\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): void {\n // Validate params\n if (endpoint.params && options.params) {\n const result = endpoint.params.safeParse(options.params);\n if (!result.success) {\n throw new ClientValidationError('params', result.error.issues);\n }\n }\n\n // Validate query\n if (endpoint.query && options.query) {\n const result = endpoint.query.safeParse(options.query);\n if (!result.success) {\n throw new ClientValidationError('query', result.error.issues);\n }\n }\n\n // Validate headers\n if (endpoint.headers && options.headers) {\n const result = endpoint.headers.safeParse(options.headers);\n if (!result.success) {\n throw new ClientValidationError('headers', result.error.issues);\n }\n }\n\n // Validate body\n if (endpoint.body && options.body) {\n const result = endpoint.body.safeParse(options.body);\n if (!result.success) {\n throw new ClientValidationError('body', result.error.issues);\n }\n }\n}\n\n/**\n * Validate response data after receiving\n */\nfunction validateResponse<T extends EndpointDefinition>(\n endpoint: T,\n status: number,\n data: unknown,\n): void {\n const responseSchema = endpoint.responses[status];\n if (responseSchema) {\n const result = responseSchema.safeParse(data);\n if (!result.success) {\n throw new ClientValidationError(`response[${status}]`, result.error.issues);\n }\n }\n}\n\n/**\n * Make a request to an endpoint\n */\nasync function makeRequest<T extends EndpointDefinition>(\n config: ClientConfig,\n endpoint: T,\n options: EndpointRequestOptions<T>,\n): Promise<EndpointResponse<T>> {\n // Validate request if enabled\n if (config.validateRequest !== false) {\n validateRequest(endpoint, options);\n }\n\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n config.baseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n // Build headers\n const headers = new Headers(config.headers);\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.set(key, String(value));\n }\n }\n\n // Build request init\n const init: RequestInit = {\n method: endpoint.method,\n headers,\n };\n\n // Add body if present\n if (options.body !== undefined) {\n headers.set('content-type', 'application/json');\n init.body = JSON.stringify(options.body);\n }\n\n // Make request\n const response = await fetch(url, init);\n\n // Parse response\n let data: unknown;\n\n // Handle 204 No Content\n if (response.status === 204) {\n data = {};\n } else {\n const contentType = response.headers.get('content-type') || '';\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else if (contentType.includes('text/')) {\n data = await response.text();\n } else {\n // Check if there's any content\n const text = await response.text();\n if (text) {\n data = text;\n } else {\n data = {};\n }\n }\n }\n\n // Check for HTTP errors\n if (!response.ok && !(response.status in endpoint.responses)) {\n throw new HTTPError(response.status, response.statusText, data);\n }\n\n // Validate response if enabled\n if (config.validateResponse !== false) {\n validateResponse(endpoint, response.status, data);\n }\n\n return {\n status: response.status,\n data,\n } as EndpointResponse<T>;\n}\n\n/**\n * Create a typesafe client for a contract\n */\nexport function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T> {\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n return makeRequest(config, endpoint, options);\n };\n }\n\n return client as Client<T>;\n}\n\n/**\n * Create a client without providing the contract at runtime\n * Useful when you only need types and want a lighter bundle\n */\nexport function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T> {\n return new Proxy({} as Client<T>, {\n get(_target, _prop: string) {\n return async (_options: EndpointRequestOptions<EndpointDefinition> = {}) => {\n // Without the contract, we can't validate or infer the endpoint\n // This is just a basic fetch wrapper with typing\n throw new Error(\n 'createTypedClient requires contract at runtime for validation. Use createClient instead.',\n );\n };\n },\n });\n}\n"
6
+ ],
7
+ "mappings": ";;AAQA;AAAA;AAsCO,MAAM,8BAA8B,MAAM;AAAA,EAEtC;AAAA,EACA;AAAA,EAFT,WAAW,CACF,OACA,QACP;AAAA,IACA,MAAM,yBAAyB,OAAO;AAAA,IAH/B;AAAA,IACA;AAAA,IAGP,KAAK,OAAO;AAAA;AAEhB;AAAA;AAGO,MAAM,kBAAkB,MAAM;AAAA,EAE1B;AAAA,EACA;AAAA,EACA;AAAA,EAHT,WAAW,CACF,QACA,YACA,MACP;AAAA,IACA,MAAM,cAAc,WAAW,YAAY;AAAA,IAJpC;AAAA,IACA;AAAA,IACA;AAAA,IAGP,KAAK,OAAO;AAAA;AAEhB;AAKA,SAAS,eAA6C,CACpD,UACA,SACM;AAAA,EAEN,IAAI,SAAS,UAAU,QAAQ,QAAQ;AAAA,IACrC,MAAM,SAAS,SAAS,OAAO,UAAU,QAAQ,MAAM;AAAA,IACvD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,UAAU,OAAO,MAAM,MAAM;AAAA,IAC/D;AAAA,EACF;AAAA,EAGA,IAAI,SAAS,SAAS,QAAQ,OAAO;AAAA,IACnC,MAAM,SAAS,SAAS,MAAM,UAAU,QAAQ,KAAK;AAAA,IACrD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,SAAS,OAAO,MAAM,MAAM;AAAA,IAC9D;AAAA,EACF;AAAA,EAGA,IAAI,SAAS,WAAW,QAAQ,SAAS;AAAA,IACvC,MAAM,SAAS,SAAS,QAAQ,UAAU,QAAQ,OAAO;AAAA,IACzD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,WAAW,OAAO,MAAM,MAAM;AAAA,IAChE;AAAA,EACF;AAAA,EAGA,IAAI,SAAS,QAAQ,QAAQ,MAAM;AAAA,IACjC,MAAM,SAAS,SAAS,KAAK,UAAU,QAAQ,IAAI;AAAA,IACnD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,QAAQ,OAAO,MAAM,MAAM;AAAA,IAC7D;AAAA,EACF;AAAA;AAMF,SAAS,gBAA8C,CACrD,UACA,QACA,MACM;AAAA,EACN,MAAM,iBAAiB,SAAS,UAAU;AAAA,EAC1C,IAAI,gBAAgB;AAAA,IAClB,MAAM,SAAS,eAAe,UAAU,IAAI;AAAA,IAC5C,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,sBAAsB,YAAY,WAAW,OAAO,MAAM,MAAM;AAAA,IAC5E;AAAA,EACF;AAAA;AAMF,eAAe,WAAyC,CACtD,QACA,UACA,SAC8B;AAAA,EAE9B,IAAI,OAAO,oBAAoB,OAAO;AAAA,IACpC,gBAAgB,UAAU,OAAO;AAAA,EACnC;AAAA,EAGA,IAAI,OAAO,SAAS;AAAA,EACpB,IAAI,QAAQ,QAAQ;AAAA,IAClB,OAAO,gBAAgB,MAAM,QAAQ,MAAyC;AAAA,EAChF;AAAA,EAEA,MAAM,MAAM,SACV,OAAO,SACP,MACA,QAAQ,KACV;AAAA,EAGA,MAAM,UAAU,IAAI,QAAQ,OAAO,OAAO;AAAA,EAC1C,IAAI,QAAQ,SAAS;AAAA,IACnB,YAAY,KAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,GAAG;AAAA,MAC1D,QAAQ,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IAChC;AAAA,EACF;AAAA,EAGA,MAAM,OAAoB;AAAA,IACxB,QAAQ,SAAS;AAAA,IACjB;AAAA,EACF;AAAA,EAGA,IAAI,QAAQ,SAAS,WAAW;AAAA,IAC9B,QAAQ,IAAI,gBAAgB,kBAAkB;AAAA,IAC9C,KAAK,OAAO,KAAK,UAAU,QAAQ,IAAI;AAAA,EACzC;AAAA,EAGA,MAAM,WAAW,MAAM,MAAM,KAAK,IAAI;AAAA,EAGtC,IAAI;AAAA,EAGJ,IAAI,SAAS,WAAW,KAAK;AAAA,IAC3B,OAAO,CAAC;AAAA,EACV,EAAO;AAAA,IACL,MAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAAA,IAE5D,IAAI,YAAY,SAAS,kBAAkB,GAAG;AAAA,MAC5C,OAAO,MAAM,SAAS,KAAK;AAAA,IAC7B,EAAO,SAAI,YAAY,SAAS,OAAO,GAAG;AAAA,MACxC,OAAO,MAAM,SAAS,KAAK;AAAA,IAC7B,EAAO;AAAA,MAEL,MAAM,OAAO,MAAM,SAAS,KAAK;AAAA,MACjC,IAAI,MAAM;AAAA,QACR,OAAO;AAAA,MACT,EAAO;AAAA,QACL,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA,EAMd,IAAI,CAAC,SAAS,MAAM,EAAE,SAAS,UAAU,SAAS,YAAY;AAAA,IAC5D,MAAM,IAAI,UAAU,SAAS,QAAQ,SAAS,YAAY,IAAI;AAAA,EAChE;AAAA,EAGA,IAAI,OAAO,qBAAqB,OAAO;AAAA,IACrC,iBAAiB,UAAU,SAAS,QAAQ,IAAI;AAAA,EAClD;AAAA,EAEA,OAAO;AAAA,IACL,QAAQ,SAAS;AAAA,IACjB;AAAA,EACF;AAAA;AAMK,SAAS,YAAgC,CAAC,UAAa,QAAiC;AAAA,EAC7F,MAAM,SAAkC,CAAC;AAAA,EAEzC,YAAY,MAAM,aAAa,OAAO,QAAQ,QAAQ,GAAG;AAAA,IACvD,OAAO,QAAQ,CAAC,UAAsD,CAAC,MAAM;AAAA,MAC3E,OAAO,YAAY,QAAQ,UAAU,OAAO;AAAA;AAAA,EAEhD;AAAA,EAEA,OAAO;AAAA;AAOF,SAAS,iBAAqC,CAAC,SAAkC;AAAA,EACtF,OAAO,IAAI,MAAM,CAAC,GAAgB;AAAA,IAChC,GAAG,CAAC,SAAS,OAAe;AAAA,MAC1B,OAAO,OAAO,WAAuD,CAAC,MAAM;AAAA,QAG1E,MAAM,IAAI,MACR,0FACF;AAAA;AAAA;AAAA,EAGN,CAAC;AAAA;",
8
+ "debugId": "1B7C82F232BA1F0964756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@richie-rpc/client",
3
+ "version": "0.1.0",
4
+ "type": "module"
5
+ }
@@ -0,0 +1,44 @@
1
+ import type { Contract, EndpointDefinition, ExtractBody, ExtractHeaders, ExtractParams, ExtractQuery } from '@richie-rpc/core';
2
+ import type { z } from 'zod';
3
+ export interface ClientConfig {
4
+ baseUrl: string;
5
+ headers?: Record<string, string>;
6
+ validateRequest?: boolean;
7
+ validateResponse?: boolean;
8
+ }
9
+ export type EndpointRequestOptions<T extends EndpointDefinition> = {
10
+ params?: ExtractParams<T> extends never ? never : ExtractParams<T>;
11
+ query?: ExtractQuery<T> extends never ? never : ExtractQuery<T>;
12
+ headers?: ExtractHeaders<T> extends never ? never : ExtractHeaders<T>;
13
+ body?: ExtractBody<T> extends never ? never : ExtractBody<T>;
14
+ };
15
+ export type EndpointResponse<T extends EndpointDefinition> = {
16
+ [Status in keyof T['responses']]: {
17
+ status: Status;
18
+ data: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;
19
+ };
20
+ }[keyof T['responses']];
21
+ export type ClientMethod<T extends EndpointDefinition> = (options: EndpointRequestOptions<T>) => Promise<EndpointResponse<T>>;
22
+ export type Client<T extends Contract> = {
23
+ [K in keyof T]: ClientMethod<T[K]>;
24
+ };
25
+ export declare class ClientValidationError extends Error {
26
+ field: string;
27
+ issues: z.ZodIssue[];
28
+ constructor(field: string, issues: z.ZodIssue[]);
29
+ }
30
+ export declare class HTTPError extends Error {
31
+ status: number;
32
+ statusText: string;
33
+ body: unknown;
34
+ constructor(status: number, statusText: string, body: unknown);
35
+ }
36
+ /**
37
+ * Create a typesafe client for a contract
38
+ */
39
+ export declare function createClient<T extends Contract>(contract: T, config: ClientConfig): Client<T>;
40
+ /**
41
+ * Create a client without providing the contract at runtime
42
+ * Useful when you only need types and want a lighter bundle
43
+ */
44
+ export declare function createTypedClient<T extends Contract>(_config: ClientConfig): Client<T>;
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@richie-rpc/client",
3
+ "version": "0.1.0",
4
+ "main": "./dist/cjs/index.cjs",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/types/index.d.ts",
8
+ "require": "./dist/cjs/index.cjs",
9
+ "import": "./dist/mjs/index.mjs"
10
+ }
11
+ },
12
+ "dependencies": {
13
+ "@richie-rpc/core": "^0.1.0",
14
+ "zod": "^3.23.8"
15
+ },
16
+ "peerDependencies": {
17
+ "typescript": "^5"
18
+ },
19
+ "author": "Richie <oss@ricsam.dev>",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ricsam/richie-rpc.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/ricsam/richie-rpc/issues"
27
+ },
28
+ "homepage": "https://github.com/ricsam/richie-rpc#readme",
29
+ "keywords": [
30
+ "typescript",
31
+ "bun",
32
+ "zod",
33
+ "api",
34
+ "contract",
35
+ "rpc",
36
+ "rest",
37
+ "openapi",
38
+ "type-safe"
39
+ ],
40
+ "description": "Type-safe fetch client for Richie RPC contracts",
41
+ "module": "./dist/mjs/index.mjs",
42
+ "types": "./dist/types/index.d.ts",
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md"
49
+ ]
50
+ }