@richie-rpc/server 1.2.3 → 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 +271 -54
- package/dist/cjs/index.cjs +263 -2
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +263 -2
- 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,6 +94,9 @@ 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
|
|
@@ -137,14 +140,15 @@ Use the `Status` const object for type-safe status codes:
|
|
|
137
140
|
```typescript
|
|
138
141
|
import { Status } from '@richie-rpc/server';
|
|
139
142
|
|
|
140
|
-
return { status: Status.OK, body: user };
|
|
141
|
-
return { status: Status.Created, body: newUser };
|
|
142
|
-
return { status: Status.NoContent, body: {} };
|
|
143
|
-
return { status: Status.BadRequest, body: error };
|
|
144
|
-
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
|
|
145
148
|
```
|
|
146
149
|
|
|
147
150
|
Available status codes in `Status` object:
|
|
151
|
+
|
|
148
152
|
- **Success**: `OK` (200), `Created` (201), `Accepted` (202), `NoContent` (204)
|
|
149
153
|
- **Redirection**: `MovedPermanently` (301), `Found` (302), `NotModified` (304)
|
|
150
154
|
- **Client Errors**: `BadRequest` (400), `Unauthorized` (401), `Forbidden` (403), `NotFound` (404), `MethodNotAllowed` (405), `Conflict` (409), `UnprocessableEntity` (422), `TooManyRequests` (429)
|
|
@@ -172,18 +176,18 @@ const contract = defineContract({
|
|
|
172
176
|
method: 'GET',
|
|
173
177
|
path: '/teapot',
|
|
174
178
|
responses: {
|
|
175
|
-
418: z.object({ message: z.string(), isTeapot: z.boolean() })
|
|
176
|
-
}
|
|
177
|
-
}
|
|
179
|
+
418: z.object({ message: z.string(), isTeapot: z.boolean() }),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
178
182
|
});
|
|
179
183
|
|
|
180
184
|
const router = createRouter(contract, {
|
|
181
185
|
teapot: async () => {
|
|
182
186
|
return {
|
|
183
187
|
status: 418 as const,
|
|
184
|
-
body: { message: "I'm a teapot", isTeapot: true }
|
|
188
|
+
body: { message: "I'm a teapot", isTeapot: true },
|
|
185
189
|
};
|
|
186
|
-
}
|
|
190
|
+
},
|
|
187
191
|
});
|
|
188
192
|
```
|
|
189
193
|
|
|
@@ -198,11 +202,13 @@ const contract = defineContract({
|
|
|
198
202
|
path: '/upload',
|
|
199
203
|
contentType: 'multipart/form-data',
|
|
200
204
|
body: z.object({
|
|
201
|
-
documents: z.array(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
),
|
|
206
212
|
category: z.string(),
|
|
207
213
|
}),
|
|
208
214
|
responses: {
|
|
@@ -240,10 +246,229 @@ const router = createRouter(contract, {
|
|
|
240
246
|
```
|
|
241
247
|
|
|
242
248
|
The server automatically:
|
|
249
|
+
|
|
243
250
|
- Parses `multipart/form-data` requests
|
|
244
251
|
- Reconstructs nested structures with File objects
|
|
245
252
|
- Validates the body against your Zod schema
|
|
246
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
|
+
},
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
247
472
|
## Error Handling
|
|
248
473
|
|
|
249
474
|
The router throws specific error classes that you can catch and handle. These errors are thrown before handlers are called, so you should wrap your router calls in try-catch blocks.
|
|
@@ -255,11 +480,13 @@ The router throws specific error classes that you can catch and handle. These er
|
|
|
255
480
|
Thrown when request or response validation fails. Contains detailed Zod validation issues.
|
|
256
481
|
|
|
257
482
|
**Properties:**
|
|
483
|
+
|
|
258
484
|
- `field: string` - The field that failed validation (`"params"`, `"query"`, `"headers"`, `"body"`, or `"response[status]"`)
|
|
259
485
|
- `issues: z.ZodIssue[]` - Array of Zod validation issues with detailed error information
|
|
260
486
|
- `message: string` - Error message
|
|
261
487
|
|
|
262
488
|
**When thrown:**
|
|
489
|
+
|
|
263
490
|
- Invalid path parameters (params)
|
|
264
491
|
- Invalid query parameters (query)
|
|
265
492
|
- Invalid request headers (headers)
|
|
@@ -287,10 +514,10 @@ Bun.serve({
|
|
|
287
514
|
field: error.field,
|
|
288
515
|
issues: error.issues,
|
|
289
516
|
},
|
|
290
|
-
{ status: 400 }
|
|
517
|
+
{ status: 400 },
|
|
291
518
|
);
|
|
292
519
|
}
|
|
293
|
-
|
|
520
|
+
|
|
294
521
|
if (error instanceof RouteNotFoundError) {
|
|
295
522
|
// Handle route not found
|
|
296
523
|
return Response.json(
|
|
@@ -298,16 +525,13 @@ Bun.serve({
|
|
|
298
525
|
error: 'Not Found',
|
|
299
526
|
message: `Route ${error.method} ${error.path} not found`,
|
|
300
527
|
},
|
|
301
|
-
{ status: 404 }
|
|
528
|
+
{ status: 404 },
|
|
302
529
|
);
|
|
303
530
|
}
|
|
304
|
-
|
|
531
|
+
|
|
305
532
|
// Handle unexpected errors
|
|
306
533
|
console.error('Unexpected error:', error);
|
|
307
|
-
return Response.json(
|
|
308
|
-
{ error: 'Internal Server Error' },
|
|
309
|
-
{ status: 500 }
|
|
310
|
-
);
|
|
534
|
+
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
311
535
|
}
|
|
312
536
|
},
|
|
313
537
|
});
|
|
@@ -318,10 +542,12 @@ Bun.serve({
|
|
|
318
542
|
Thrown when no matching route is found for the request.
|
|
319
543
|
|
|
320
544
|
**Properties:**
|
|
545
|
+
|
|
321
546
|
- `path: string` - The requested path
|
|
322
547
|
- `method: string` - The HTTP method (GET, POST, etc.)
|
|
323
548
|
|
|
324
549
|
**When thrown:**
|
|
550
|
+
|
|
325
551
|
- No endpoint in the contract matches the request method and path
|
|
326
552
|
|
|
327
553
|
**Example:**
|
|
@@ -336,7 +562,7 @@ try {
|
|
|
336
562
|
error: 'Not Found',
|
|
337
563
|
message: `Cannot ${error.method} ${error.path}`,
|
|
338
564
|
},
|
|
339
|
-
{ status: 404 }
|
|
565
|
+
{ status: 404 },
|
|
340
566
|
);
|
|
341
567
|
}
|
|
342
568
|
throw error; // Re-throw other errors
|
|
@@ -346,12 +572,7 @@ try {
|
|
|
346
572
|
### Complete Error Handling Example
|
|
347
573
|
|
|
348
574
|
```typescript
|
|
349
|
-
import {
|
|
350
|
-
createRouter,
|
|
351
|
-
ValidationError,
|
|
352
|
-
RouteNotFoundError,
|
|
353
|
-
Status,
|
|
354
|
-
} from '@richie-rpc/server';
|
|
575
|
+
import { createRouter, ValidationError, RouteNotFoundError, Status } from '@richie-rpc/server';
|
|
355
576
|
|
|
356
577
|
const router = createRouter(contract, handlers);
|
|
357
578
|
|
|
@@ -359,7 +580,7 @@ Bun.serve({
|
|
|
359
580
|
port: 3000,
|
|
360
581
|
async fetch(request) {
|
|
361
582
|
const url = new URL(request.url);
|
|
362
|
-
|
|
583
|
+
|
|
363
584
|
// Handle API routes
|
|
364
585
|
if (url.pathname.startsWith('/api/')) {
|
|
365
586
|
try {
|
|
@@ -377,29 +598,26 @@ Bun.serve({
|
|
|
377
598
|
code: issue.code,
|
|
378
599
|
})),
|
|
379
600
|
},
|
|
380
|
-
{ status: 400 }
|
|
601
|
+
{ status: 400 },
|
|
381
602
|
);
|
|
382
603
|
}
|
|
383
|
-
|
|
604
|
+
|
|
384
605
|
if (error instanceof RouteNotFoundError) {
|
|
385
606
|
return Response.json(
|
|
386
607
|
{
|
|
387
608
|
error: 'Not Found',
|
|
388
609
|
message: `Route ${error.method} ${error.path} not found`,
|
|
389
610
|
},
|
|
390
|
-
{ status: 404 }
|
|
611
|
+
{ status: 404 },
|
|
391
612
|
);
|
|
392
613
|
}
|
|
393
|
-
|
|
614
|
+
|
|
394
615
|
// Log unexpected errors
|
|
395
616
|
console.error('Unexpected error:', error);
|
|
396
|
-
return Response.json(
|
|
397
|
-
{ error: 'Internal Server Error' },
|
|
398
|
-
{ status: 500 }
|
|
399
|
-
);
|
|
617
|
+
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
400
618
|
}
|
|
401
619
|
}
|
|
402
|
-
|
|
620
|
+
|
|
403
621
|
// Handle other routes
|
|
404
622
|
return new Response('Not Found', { status: 404 });
|
|
405
623
|
},
|
|
@@ -447,4 +665,3 @@ Both request and response data are validated against the contract schemas:
|
|
|
447
665
|
## License
|
|
448
666
|
|
|
449
667
|
MIT
|
|
450
|
-
|