@richie-rpc/server 1.2.2 → 1.2.4
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 +325 -50
- package/dist/cjs/index.cjs +263 -5
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +264 -6
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/types/index.d.ts +96 -5
- package/dist/types/websocket.d.ts +125 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,19 +20,19 @@ const router = createRouter(contract, {
|
|
|
20
20
|
getUser: async ({ params }) => {
|
|
21
21
|
// params is fully typed based on the contract
|
|
22
22
|
const user = await db.getUser(params.id);
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
if (!user) {
|
|
25
25
|
return { status: Status.NotFound, body: { error: 'User not found' } };
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
return { status: Status.OK, body: user };
|
|
29
29
|
},
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
createUser: async ({ body }) => {
|
|
32
32
|
// body is fully typed and already validated
|
|
33
33
|
const user = await db.createUser(body);
|
|
34
34
|
return { status: Status.Created, body: user };
|
|
35
|
-
}
|
|
35
|
+
},
|
|
36
36
|
});
|
|
37
37
|
```
|
|
38
38
|
|
|
@@ -41,22 +41,22 @@ const router = createRouter(contract, {
|
|
|
41
41
|
You can serve your API under a path prefix (e.g., `/api`) using the `basePath` option:
|
|
42
42
|
|
|
43
43
|
```typescript
|
|
44
|
-
const router = createRouter(contract, handlers, {
|
|
45
|
-
basePath: '/api'
|
|
44
|
+
const router = createRouter(contract, handlers, {
|
|
45
|
+
basePath: '/api',
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
Bun.serve({
|
|
49
49
|
port: 3000,
|
|
50
50
|
fetch(request) {
|
|
51
51
|
const url = new URL(request.url);
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
// Route all /api/* requests to the router
|
|
54
54
|
if (url.pathname.startsWith('/api/')) {
|
|
55
55
|
return router.fetch(request);
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
return new Response('Not Found', { status: 404 });
|
|
59
|
-
}
|
|
59
|
+
},
|
|
60
60
|
});
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -67,7 +67,7 @@ The router will automatically strip the basePath prefix before matching routes.
|
|
|
67
67
|
```typescript
|
|
68
68
|
Bun.serve({
|
|
69
69
|
port: 3000,
|
|
70
|
-
fetch: router.fetch
|
|
70
|
+
fetch: router.fetch,
|
|
71
71
|
});
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -78,13 +78,13 @@ Bun.serve({
|
|
|
78
78
|
port: 3000,
|
|
79
79
|
fetch(request) {
|
|
80
80
|
const url = new URL(request.url);
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
if (url.pathname.startsWith('/api')) {
|
|
83
83
|
return router.handle(request);
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
return new Response('Not Found', { status: 404 });
|
|
87
|
-
}
|
|
87
|
+
},
|
|
88
88
|
});
|
|
89
89
|
```
|
|
90
90
|
|
|
@@ -94,10 +94,14 @@ Bun.serve({
|
|
|
94
94
|
- ✅ Automatic response validation
|
|
95
95
|
- ✅ Type-safe handler inputs
|
|
96
96
|
- ✅ Type-safe status codes with `Status` const object
|
|
97
|
+
- ✅ HTTP Streaming with `StreamEmitter`
|
|
98
|
+
- ✅ Server-Sent Events with `SSEEmitter`
|
|
99
|
+
- ✅ WebSocket router with pub/sub support
|
|
97
100
|
- ✅ Path parameter matching
|
|
98
101
|
- ✅ Query parameter parsing
|
|
99
102
|
- ✅ JSON body parsing
|
|
100
|
-
- ✅
|
|
103
|
+
- ✅ File uploads with `multipart/form-data`
|
|
104
|
+
- ✅ Nested file structures in request bodies
|
|
101
105
|
- ✅ BasePath support for serving APIs under path prefixes
|
|
102
106
|
- ✅ Detailed validation errors
|
|
103
107
|
- ✅ 404 handling for unknown routes
|
|
@@ -136,14 +140,15 @@ Use the `Status` const object for type-safe status codes:
|
|
|
136
140
|
```typescript
|
|
137
141
|
import { Status } from '@richie-rpc/server';
|
|
138
142
|
|
|
139
|
-
return { status: Status.OK, body: user };
|
|
140
|
-
return { status: Status.Created, body: newUser };
|
|
141
|
-
return { status: Status.NoContent, body: {} };
|
|
142
|
-
return { status: Status.BadRequest, body: error };
|
|
143
|
-
return { status: Status.NotFound, body: error };
|
|
143
|
+
return { status: Status.OK, body: user }; // 200
|
|
144
|
+
return { status: Status.Created, body: newUser }; // 201
|
|
145
|
+
return { status: Status.NoContent, body: {} }; // 204
|
|
146
|
+
return { status: Status.BadRequest, body: error }; // 400
|
|
147
|
+
return { status: Status.NotFound, body: error }; // 404
|
|
144
148
|
```
|
|
145
149
|
|
|
146
150
|
Available status codes in `Status` object:
|
|
151
|
+
|
|
147
152
|
- **Success**: `OK` (200), `Created` (201), `Accepted` (202), `NoContent` (204)
|
|
148
153
|
- **Redirection**: `MovedPermanently` (301), `Found` (302), `NotModified` (304)
|
|
149
154
|
- **Client Errors**: `BadRequest` (400), `Unauthorized` (401), `Forbidden` (403), `NotFound` (404), `MethodNotAllowed` (405), `Conflict` (409), `UnprocessableEntity` (422), `TooManyRequests` (429)
|
|
@@ -171,18 +176,296 @@ const contract = defineContract({
|
|
|
171
176
|
method: 'GET',
|
|
172
177
|
path: '/teapot',
|
|
173
178
|
responses: {
|
|
174
|
-
418: z.object({ message: z.string(), isTeapot: z.boolean() })
|
|
175
|
-
}
|
|
176
|
-
}
|
|
179
|
+
418: z.object({ message: z.string(), isTeapot: z.boolean() }),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
177
182
|
});
|
|
178
183
|
|
|
179
184
|
const router = createRouter(contract, {
|
|
180
185
|
teapot: async () => {
|
|
181
186
|
return {
|
|
182
187
|
status: 418 as const,
|
|
183
|
-
body: { message: "I'm a teapot", isTeapot: true }
|
|
188
|
+
body: { message: "I'm a teapot", isTeapot: true },
|
|
184
189
|
};
|
|
185
|
-
}
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Handling File Uploads
|
|
195
|
+
|
|
196
|
+
The server automatically handles `multipart/form-data` requests when the contract specifies `contentType: 'multipart/form-data'`. File objects are fully reconstructed and passed to your handler:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const contract = defineContract({
|
|
200
|
+
uploadDocuments: {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
path: '/upload',
|
|
203
|
+
contentType: 'multipart/form-data',
|
|
204
|
+
body: z.object({
|
|
205
|
+
documents: z.array(
|
|
206
|
+
z.object({
|
|
207
|
+
file: z.instanceof(File),
|
|
208
|
+
name: z.string(),
|
|
209
|
+
tags: z.array(z.string()).optional(),
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
212
|
+
category: z.string(),
|
|
213
|
+
}),
|
|
214
|
+
responses: {
|
|
215
|
+
[Status.Created]: z.object({
|
|
216
|
+
uploadedCount: z.number(),
|
|
217
|
+
totalSize: z.number(),
|
|
218
|
+
}),
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const router = createRouter(contract, {
|
|
224
|
+
uploadDocuments: async ({ body }) => {
|
|
225
|
+
// body.documents is fully typed with File objects
|
|
226
|
+
let totalSize = 0;
|
|
227
|
+
|
|
228
|
+
for (const doc of body.documents) {
|
|
229
|
+
// doc.file is a File object
|
|
230
|
+
const buffer = await doc.file.arrayBuffer();
|
|
231
|
+
totalSize += buffer.byteLength;
|
|
232
|
+
|
|
233
|
+
console.log(`Processing: ${doc.name} (${doc.file.name})`);
|
|
234
|
+
console.log(`Tags: ${doc.tags?.join(', ') ?? 'none'}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
status: Status.Created,
|
|
239
|
+
body: {
|
|
240
|
+
uploadedCount: body.documents.length,
|
|
241
|
+
totalSize,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The server automatically:
|
|
249
|
+
|
|
250
|
+
- Parses `multipart/form-data` requests
|
|
251
|
+
- Reconstructs nested structures with File objects
|
|
252
|
+
- Validates the body against your Zod schema
|
|
253
|
+
|
|
254
|
+
## Streaming Responses
|
|
255
|
+
|
|
256
|
+
For AI-style streaming responses, handlers receive a `stream` object:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
const contract = defineContract({
|
|
260
|
+
generateText: {
|
|
261
|
+
type: 'streaming',
|
|
262
|
+
method: 'POST',
|
|
263
|
+
path: '/generate',
|
|
264
|
+
body: z.object({ prompt: z.string() }),
|
|
265
|
+
chunk: z.object({ text: z.string() }),
|
|
266
|
+
finalResponse: z.object({ totalTokens: z.number() }),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const router = createRouter(contract, {
|
|
271
|
+
generateText: async ({ body, stream }) => {
|
|
272
|
+
const words = body.prompt.split(' ');
|
|
273
|
+
|
|
274
|
+
for (const word of words) {
|
|
275
|
+
if (!stream.isOpen) return; // Client disconnected
|
|
276
|
+
stream.send({ text: word + ' ' });
|
|
277
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
stream.close({ totalTokens: words.length });
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### StreamEmitter Interface
|
|
286
|
+
|
|
287
|
+
- `send(chunk)` - Send a chunk to the client (NDJSON format)
|
|
288
|
+
- `close(finalResponse?)` - Close the stream with optional final response
|
|
289
|
+
- `isOpen` - Check if the stream is still open
|
|
290
|
+
|
|
291
|
+
## Server-Sent Events (SSE)
|
|
292
|
+
|
|
293
|
+
For server-to-client event streaming, handlers receive an `emitter` object:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const contract = defineContract({
|
|
297
|
+
notifications: {
|
|
298
|
+
type: 'sse',
|
|
299
|
+
method: 'GET',
|
|
300
|
+
path: '/notifications',
|
|
301
|
+
events: {
|
|
302
|
+
message: z.object({ text: z.string() }),
|
|
303
|
+
heartbeat: z.object({ timestamp: z.string() }),
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const router = createRouter(contract, {
|
|
309
|
+
notifications: ({ emitter, signal }) => {
|
|
310
|
+
// Send heartbeats every 30 seconds
|
|
311
|
+
const heartbeatInterval = setInterval(() => {
|
|
312
|
+
if (!emitter.isOpen) return;
|
|
313
|
+
emitter.send('heartbeat', { timestamp: new Date().toISOString() });
|
|
314
|
+
}, 30000);
|
|
315
|
+
|
|
316
|
+
// Cleanup when client disconnects
|
|
317
|
+
signal.addEventListener('abort', () => {
|
|
318
|
+
clearInterval(heartbeatInterval);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Optional: return cleanup function
|
|
322
|
+
return () => clearInterval(heartbeatInterval);
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### SSEEmitter Interface
|
|
328
|
+
|
|
329
|
+
- `send(event, data, options?)` - Send an event with data and optional ID
|
|
330
|
+
- `close()` - Close the connection
|
|
331
|
+
- `isOpen` - Check if the connection is still open
|
|
332
|
+
|
|
333
|
+
## WebSocket Router
|
|
334
|
+
|
|
335
|
+
For bidirectional real-time communication, use `createWebSocketRouter`:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import { createWebSocketRouter } from '@richie-rpc/server';
|
|
339
|
+
import { defineWebSocketContract } from '@richie-rpc/core';
|
|
340
|
+
|
|
341
|
+
const wsContract = defineWebSocketContract({
|
|
342
|
+
chat: {
|
|
343
|
+
path: '/ws/chat/:roomId',
|
|
344
|
+
params: z.object({ roomId: z.string() }),
|
|
345
|
+
clientMessages: {
|
|
346
|
+
sendMessage: { payload: z.object({ text: z.string() }) },
|
|
347
|
+
},
|
|
348
|
+
serverMessages: {
|
|
349
|
+
message: { payload: z.object({ userId: z.string(), text: z.string() }) },
|
|
350
|
+
error: { payload: z.object({ code: z.string(), message: z.string() }) },
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const wsRouter = createWebSocketRouter(wsContract, {
|
|
356
|
+
chat: {
|
|
357
|
+
open(ws) {
|
|
358
|
+
// Called when connection opens
|
|
359
|
+
ws.subscribe(`room:${ws.data.params.roomId}`);
|
|
360
|
+
console.log('User joined room:', ws.data.params.roomId);
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
message(ws, msg) {
|
|
364
|
+
// Called for each validated client message
|
|
365
|
+
if (msg.type === 'sendMessage') {
|
|
366
|
+
ws.publish(`room:${ws.data.params.roomId}`, 'message', {
|
|
367
|
+
userId: 'user1',
|
|
368
|
+
text: msg.payload.text,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
close(ws) {
|
|
374
|
+
// Called when connection closes
|
|
375
|
+
console.log('User left room:', ws.data.params.roomId);
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
validationError(ws, error) {
|
|
379
|
+
// Called when client message validation fails
|
|
380
|
+
ws.send('error', { code: 'VALIDATION_ERROR', message: error.message });
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Typed Per-Connection State
|
|
387
|
+
|
|
388
|
+
You can store typed per-connection state by defining a state interface and passing it as an option. This follows the same pattern as Bun's WebSocket data:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Define your per-connection state type
|
|
392
|
+
interface ChatConnectionState {
|
|
393
|
+
connectionId: string;
|
|
394
|
+
userId?: string;
|
|
395
|
+
username?: string;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const wsRouter = createWebSocketRouter(
|
|
399
|
+
wsContract,
|
|
400
|
+
{
|
|
401
|
+
chat: {
|
|
402
|
+
open(ws) {
|
|
403
|
+
// Initialize typed state - fully typed, no casts needed
|
|
404
|
+
ws.data.state.connectionId = generateId();
|
|
405
|
+
ws.subscribe(`room:${ws.data.params.roomId}`);
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
message(ws, msg) {
|
|
409
|
+
// Access typed state
|
|
410
|
+
const { connectionId, username } = ws.data.state;
|
|
411
|
+
|
|
412
|
+
if (msg.type === 'join') {
|
|
413
|
+
// Update state
|
|
414
|
+
ws.data.state.username = msg.payload.username;
|
|
415
|
+
ws.data.state.userId = connectionId;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (msg.type === 'sendMessage' && username) {
|
|
419
|
+
ws.publish(`room:${ws.data.params.roomId}`, 'message', {
|
|
420
|
+
userId: connectionId,
|
|
421
|
+
text: msg.payload.text,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
close(ws) {
|
|
427
|
+
// State is available throughout connection lifecycle
|
|
428
|
+
console.log(`User ${ws.data.state.username} disconnected`);
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
// Pass state type hint as third argument
|
|
433
|
+
{ state: {} as ChatConnectionState },
|
|
434
|
+
);
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
The state object is initialized as an empty object for each connection and is available via `ws.data.state` in all handlers (`open`, `message`, `close`, `validationError`).
|
|
438
|
+
|
|
439
|
+
### TypedServerWebSocket Interface
|
|
440
|
+
|
|
441
|
+
- `send(type, payload)` - Send a typed message to the client
|
|
442
|
+
- `subscribe(topic)` - Subscribe to a pub/sub topic
|
|
443
|
+
- `unsubscribe(topic)` - Unsubscribe from a topic
|
|
444
|
+
- `publish(topic, type, payload)` - Broadcast to all subscribers
|
|
445
|
+
- `close(code?, reason?)` - Close the connection
|
|
446
|
+
- `data.params` - Access path parameters from the connection
|
|
447
|
+
- `data.query` - Access query parameters from the connection
|
|
448
|
+
- `data.headers` - Access headers from the connection
|
|
449
|
+
- `data.state` - Access typed per-connection state (see above)
|
|
450
|
+
|
|
451
|
+
### Integrating with Bun.serve
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
Bun.serve({
|
|
455
|
+
port: 3000,
|
|
456
|
+
|
|
457
|
+
websocket: wsRouter.websocketHandler,
|
|
458
|
+
|
|
459
|
+
async fetch(request, server) {
|
|
460
|
+
// Try WebSocket upgrade
|
|
461
|
+
const wsMatch = await wsRouter.matchAndPrepareUpgrade(request);
|
|
462
|
+
if (wsMatch && request.headers.get('upgrade') === 'websocket') {
|
|
463
|
+
if (server.upgrade(request, { data: wsMatch })) return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Handle regular HTTP requests
|
|
467
|
+
return router.fetch(request);
|
|
468
|
+
},
|
|
186
469
|
});
|
|
187
470
|
```
|
|
188
471
|
|
|
@@ -197,11 +480,13 @@ The router throws specific error classes that you can catch and handle. These er
|
|
|
197
480
|
Thrown when request or response validation fails. Contains detailed Zod validation issues.
|
|
198
481
|
|
|
199
482
|
**Properties:**
|
|
483
|
+
|
|
200
484
|
- `field: string` - The field that failed validation (`"params"`, `"query"`, `"headers"`, `"body"`, or `"response[status]"`)
|
|
201
485
|
- `issues: z.ZodIssue[]` - Array of Zod validation issues with detailed error information
|
|
202
486
|
- `message: string` - Error message
|
|
203
487
|
|
|
204
488
|
**When thrown:**
|
|
489
|
+
|
|
205
490
|
- Invalid path parameters (params)
|
|
206
491
|
- Invalid query parameters (query)
|
|
207
492
|
- Invalid request headers (headers)
|
|
@@ -229,10 +514,10 @@ Bun.serve({
|
|
|
229
514
|
field: error.field,
|
|
230
515
|
issues: error.issues,
|
|
231
516
|
},
|
|
232
|
-
{ status: 400 }
|
|
517
|
+
{ status: 400 },
|
|
233
518
|
);
|
|
234
519
|
}
|
|
235
|
-
|
|
520
|
+
|
|
236
521
|
if (error instanceof RouteNotFoundError) {
|
|
237
522
|
// Handle route not found
|
|
238
523
|
return Response.json(
|
|
@@ -240,16 +525,13 @@ Bun.serve({
|
|
|
240
525
|
error: 'Not Found',
|
|
241
526
|
message: `Route ${error.method} ${error.path} not found`,
|
|
242
527
|
},
|
|
243
|
-
{ status: 404 }
|
|
528
|
+
{ status: 404 },
|
|
244
529
|
);
|
|
245
530
|
}
|
|
246
|
-
|
|
531
|
+
|
|
247
532
|
// Handle unexpected errors
|
|
248
533
|
console.error('Unexpected error:', error);
|
|
249
|
-
return Response.json(
|
|
250
|
-
{ error: 'Internal Server Error' },
|
|
251
|
-
{ status: 500 }
|
|
252
|
-
);
|
|
534
|
+
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
253
535
|
}
|
|
254
536
|
},
|
|
255
537
|
});
|
|
@@ -260,10 +542,12 @@ Bun.serve({
|
|
|
260
542
|
Thrown when no matching route is found for the request.
|
|
261
543
|
|
|
262
544
|
**Properties:**
|
|
545
|
+
|
|
263
546
|
- `path: string` - The requested path
|
|
264
547
|
- `method: string` - The HTTP method (GET, POST, etc.)
|
|
265
548
|
|
|
266
549
|
**When thrown:**
|
|
550
|
+
|
|
267
551
|
- No endpoint in the contract matches the request method and path
|
|
268
552
|
|
|
269
553
|
**Example:**
|
|
@@ -278,7 +562,7 @@ try {
|
|
|
278
562
|
error: 'Not Found',
|
|
279
563
|
message: `Cannot ${error.method} ${error.path}`,
|
|
280
564
|
},
|
|
281
|
-
{ status: 404 }
|
|
565
|
+
{ status: 404 },
|
|
282
566
|
);
|
|
283
567
|
}
|
|
284
568
|
throw error; // Re-throw other errors
|
|
@@ -288,12 +572,7 @@ try {
|
|
|
288
572
|
### Complete Error Handling Example
|
|
289
573
|
|
|
290
574
|
```typescript
|
|
291
|
-
import {
|
|
292
|
-
createRouter,
|
|
293
|
-
ValidationError,
|
|
294
|
-
RouteNotFoundError,
|
|
295
|
-
Status,
|
|
296
|
-
} from '@richie-rpc/server';
|
|
575
|
+
import { createRouter, ValidationError, RouteNotFoundError, Status } from '@richie-rpc/server';
|
|
297
576
|
|
|
298
577
|
const router = createRouter(contract, handlers);
|
|
299
578
|
|
|
@@ -301,7 +580,7 @@ Bun.serve({
|
|
|
301
580
|
port: 3000,
|
|
302
581
|
async fetch(request) {
|
|
303
582
|
const url = new URL(request.url);
|
|
304
|
-
|
|
583
|
+
|
|
305
584
|
// Handle API routes
|
|
306
585
|
if (url.pathname.startsWith('/api/')) {
|
|
307
586
|
try {
|
|
@@ -319,29 +598,26 @@ Bun.serve({
|
|
|
319
598
|
code: issue.code,
|
|
320
599
|
})),
|
|
321
600
|
},
|
|
322
|
-
{ status: 400 }
|
|
601
|
+
{ status: 400 },
|
|
323
602
|
);
|
|
324
603
|
}
|
|
325
|
-
|
|
604
|
+
|
|
326
605
|
if (error instanceof RouteNotFoundError) {
|
|
327
606
|
return Response.json(
|
|
328
607
|
{
|
|
329
608
|
error: 'Not Found',
|
|
330
609
|
message: `Route ${error.method} ${error.path} not found`,
|
|
331
610
|
},
|
|
332
|
-
{ status: 404 }
|
|
611
|
+
{ status: 404 },
|
|
333
612
|
);
|
|
334
613
|
}
|
|
335
|
-
|
|
614
|
+
|
|
336
615
|
// Log unexpected errors
|
|
337
616
|
console.error('Unexpected error:', error);
|
|
338
|
-
return Response.json(
|
|
339
|
-
{ error: 'Internal Server Error' },
|
|
340
|
-
{ status: 500 }
|
|
341
|
-
);
|
|
617
|
+
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
342
618
|
}
|
|
343
619
|
}
|
|
344
|
-
|
|
620
|
+
|
|
345
621
|
// Handle other routes
|
|
346
622
|
return new Response('Not Found', { status: 404 });
|
|
347
623
|
},
|
|
@@ -389,4 +665,3 @@ Both request and response data are validated against the contract schemas:
|
|
|
389
665
|
## License
|
|
390
666
|
|
|
391
667
|
MIT
|
|
392
|
-
|