@grest-ts/http 0.0.6 → 0.0.7

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 CHANGED
@@ -3,616 +3,616 @@
3
3
  > [Documentation](https://github.com/grest-ts/grest-ts#readme) | [All packages](https://github.com/grest-ts/grest-ts#package-reference)
4
4
  <!-- GREST-TS-BANNER-END -->
5
5
 
6
- # HTTP Package Usage (@grest-ts/http)
7
-
8
- How to use the HTTP package for building type-safe HTTP and WebSocket APIs.
9
-
10
- ## HTTP API Definition
11
-
12
- ### Basic API Structure
13
-
14
- ```typescript
15
- // MyApi.ts
16
- import { GGRpc, httpApi } from "@grest-ts/http"
17
- import { IsArray, IsObject, IsString, IsBoolean, IsUint } from "@grest-ts/schema"
18
- import { defineApi, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR } from "@grest-ts/contract"
19
-
20
- // ---------------------------------------------------------
21
- // Type Schemas
22
- // ---------------------------------------------------------
23
-
24
- export const IsItemId = IsString.brand("ItemId")
25
- export type tItemId = typeof IsItemId.infer
26
-
27
- export const IsItem = IsObject({
28
- id: IsItemId,
29
- title: IsString,
30
- description: IsString.orUndefined,
31
- done: IsBoolean,
32
- createdAt: IsUint,
33
- updatedAt: IsUint
34
- })
35
- export type Item = typeof IsItem.infer
36
-
37
- export const IsCreateItemRequest = IsObject({
38
- title: IsString.nonEmpty,
39
- description: IsString.orUndefined
40
- })
41
- export type CreateItemRequest = typeof IsCreateItemRequest.infer
42
-
43
- export const IsUpdateItemRequest = IsObject({
44
- id: IsItemId,
45
- title: IsString.orUndefined,
46
- description: IsString.orUndefined
47
- })
48
- export type UpdateItemRequest = typeof IsUpdateItemRequest.infer
49
-
50
- export const IsItemIdParam = IsObject({
51
- id: IsItemId
52
- })
53
-
54
- // ---------------------------------------------------------
55
- // Contract & API
56
- // ---------------------------------------------------------
57
-
58
- export const MyApiContract = defineApi("MyApi", () => ({
59
- list: {
60
- success: IsArray(IsItem),
61
- errors: [SERVER_ERROR]
62
- },
63
- get: {
64
- input: IsItemIdParam,
65
- success: IsItem,
66
- errors: [NOT_FOUND, SERVER_ERROR]
67
- },
68
- create: {
69
- input: IsCreateItemRequest,
70
- success: IsItem,
71
- errors: [VALIDATION_ERROR, SERVER_ERROR]
72
- },
73
- update: {
74
- input: IsUpdateItemRequest,
75
- success: IsItem,
76
- errors: [NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR]
77
- },
78
- delete: {
79
- input: IsItemIdParam,
80
- success: undefined as undefined,
81
- errors: [NOT_FOUND, SERVER_ERROR]
82
- }
83
- }))
84
-
85
- export const MyApi = httpApi(MyApiContract)
86
- .pathPrefix("api/items")
87
- .routes({
88
- list: GGRpc.GET("list"),
89
- get: GGRpc.GET("get/:id"),
90
- create: GGRpc.POST("create"),
91
- update: GGRpc.PUT("update"),
92
- delete: GGRpc.DELETE("delete/:id")
93
- })
94
- ```
95
-
96
- ### HTTP Methods
97
-
98
- ```typescript
99
- GGRpc.GET("path") // GET request
100
- GGRpc.POST("path") // POST request
101
- GGRpc.PUT("path") // PUT request
102
- GGRpc.PATCH("path") // PATCH request
103
- GGRpc.DELETE("path") // DELETE request
104
- ```
105
-
106
- ### Path Parameters
107
-
108
- Use `:paramName` in paths - parameters are matched by position:
109
-
110
- ```typescript
111
- export const MyApiContract = defineApi("MyApi", () => ({
112
- getUser: {
113
- input: IsObject({ userId: IsUserId }),
114
- success: IsUser,
115
- errors: [NOT_FOUND, SERVER_ERROR]
116
- },
117
- getUserPost: {
118
- input: IsObject({ userId: IsUserId, postId: IsPostId }),
119
- success: IsPost,
120
- errors: [NOT_FOUND, SERVER_ERROR]
121
- }
122
- }))
123
-
124
- export const MyApi = httpApi(MyApiContract)
125
- .pathPrefix("api")
126
- .routes({
127
- getUser: GGRpc.GET("users/:userId"),
128
- getUserPost: GGRpc.GET("users/:userId/posts/:postId")
129
- })
130
- ```
131
-
132
- ### Query Parameters
133
-
134
- For GET/DELETE, object parameters become query strings:
135
-
136
- ```typescript
137
- export const MyApiContract = defineApi("MyApi", () => ({
138
- search: {
139
- input: IsObject({
140
- term: IsString,
141
- page: IsUint.orUndefined,
142
- limit: IsUint.orUndefined
143
- }),
144
- success: IsSearchResults,
145
- errors: [SERVER_ERROR]
146
- }
147
- }))
148
-
149
- // Client usage: client.search({ term: "foo", page: 1 })
150
- // Results in: GET /api/search?term=foo&page=1
151
- ```
152
-
153
- ### Request Body
154
-
155
- For POST/PUT/PATCH, the input becomes the JSON body:
156
-
157
- ```typescript
158
- export const MyApiContract = defineApi("MyApi", () => ({
159
- create: {
160
- input: IsCreateRequest,
161
- success: IsItem,
162
- errors: [VALIDATION_ERROR, SERVER_ERROR]
163
- },
164
- update: {
165
- input: IsUpdateRequest,
166
- success: IsItem,
167
- errors: [VALIDATION_ERROR, SERVER_ERROR]
168
- }
169
- }))
170
- ```
171
-
172
- ## Authentication & Context
173
-
174
- ### Using Codec (Recommended for Header-Based Auth)
175
-
176
- ```typescript
177
- // auth/UserAuth.ts
178
- import { GGContextKey } from "@grest-ts/context"
179
- import { IsObject, IsString } from "@grest-ts/schema"
180
-
181
- export const IsUserAuthToken = IsString.brand("UserAuthToken")
182
- export type tUserAuthToken = typeof IsUserAuthToken.infer
183
-
184
- export const IsUserId = IsString.brand("UserId")
185
- export type tUserId = typeof IsUserId.infer
186
-
187
- export const IsUser = IsObject({
188
- id: IsUserId,
189
- username: IsString,
190
- email: IsString
191
- })
192
- export type User = typeof IsUser.infer
193
-
194
- // Define the context value schema
195
- const IsUserAuthContext = IsObject({
196
- token: IsUserAuthToken
197
- })
198
- export type UserAuthContext = typeof IsUserAuthContext.infer
199
-
200
- // Define the header schema
201
- const HEADER_AUTHORIZATION = "authorization"
202
- const HeaderType = IsObject({
203
- [HEADER_AUTHORIZATION]: IsString.orUndefined
204
- })
205
-
206
- // Create context key with codec
207
- export const GG_USER_AUTH = new GGContextKey<UserAuthContext>("user_auth", IsUserAuthContext)
208
- GG_USER_AUTH.addCodec("http", HeaderType.codecTo(IsUserAuthContext, {
209
- encode: (headers) => {
210
- const authHeader = headers[HEADER_AUTHORIZATION]
211
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
212
- return { token: undefined as any } // Will fail validation if required
213
- }
214
- return { token: authHeader.substring(7) as tUserAuthToken }
215
- },
216
- decode: (value) => {
217
- return { [HEADER_AUTHORIZATION]: value.token ? `Bearer ${value.token}` : undefined }
218
- }
219
- }))
220
- ```
221
-
222
- ### Using Middleware (For Complex Logic)
223
-
224
- ```typescript
225
- // middleware/ClientInfoMiddleware.ts
226
- import { GGHttpRequest, GGHttpTransportMiddleware } from "@grest-ts/http"
227
- import { GGContextKey } from "@grest-ts/context"
228
- import { IsObject, IsString, IsLiteral } from "@grest-ts/schema"
229
-
230
- export interface ClientInfo {
231
- version: string
232
- platform: 'web' | 'ios' | 'android'
233
- }
234
-
235
- export const GG_CLIENT_INFO = new GGContextKey<ClientInfo>('clientInfo', IsObject({
236
- version: IsString,
237
- platform: IsLiteral("web", "ios", "android")
238
- }))
239
-
240
- export const ClientInfoMiddleware: GGHttpTransportMiddleware = {
241
- updateRequest(req: GGHttpRequest): void {
242
- const info = GG_CLIENT_INFO.get()
243
- if (info) {
244
- req.headers['x-client-version'] = info.version
245
- req.headers['x-client-platform'] = info.platform
246
- }
247
- },
248
- parseRequest(req: GGHttpRequest): void {
249
- GG_CLIENT_INFO.set({
250
- version: req.headers['x-client-version'] ?? 'unknown',
251
- platform: (req.headers['x-client-platform'] ?? 'web') as ClientInfo['platform']
252
- })
253
- }
254
- }
255
- ```
256
-
257
- ### Adding Auth/Context to API
258
-
259
- ```typescript
260
- import { GG_USER_AUTH } from "./auth/UserAuth"
261
- import { ClientInfoMiddleware } from "./middleware/ClientInfoMiddleware"
262
- import { GG_INTL_LOCALE } from "@grest-ts/intl"
263
-
264
- export const MyApiContract = defineApi("MyApi", () => ({
265
- list: {
266
- success: IsArray(IsItem),
267
- errors: [NOT_AUTHORIZED, SERVER_ERROR]
268
- },
269
- create: {
270
- input: IsCreateRequest,
271
- success: IsItem,
272
- errors: [NOT_AUTHORIZED, VALIDATION_ERROR, SERVER_ERROR]
273
- }
274
- }))
275
-
276
- // Chain multiple context providers
277
- export const MyApi = httpApi(MyApiContract)
278
- .pathPrefix("api/items")
279
- .useHeader(GG_INTL_LOCALE) // Use codec from context key
280
- .useHeader(GG_USER_AUTH) // Use codec from context key
281
- .use(ClientInfoMiddleware) // Use middleware object
282
- .routes({
283
- list: GGRpc.GET("list"),
284
- create: GGRpc.POST("create")
285
- })
286
- ```
287
-
288
- ### Public API (No Auth)
289
-
290
- ```typescript
291
- export const PublicApiContract = defineApi("PublicApi", () => ({
292
- status: {
293
- success: IsStatusResponse,
294
- errors: [SERVER_ERROR]
295
- },
296
- login: {
297
- input: IsLoginRequest,
298
- success: IsLoginResponse,
299
- errors: [VALIDATION_ERROR, SERVER_ERROR]
300
- }
301
- }))
302
-
303
- export const PublicApi = httpApi(PublicApiContract)
304
- .pathPrefix("pub")
305
- .routes({
306
- status: GGRpc.GET("status"),
307
- login: GGRpc.POST("login")
308
- })
309
- ```
310
-
311
- ## Error Types
312
-
313
- ### Declaring Errors in Contract
314
-
315
- ```typescript
316
- import { defineApi, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR, BAD_REQUEST } from "@grest-ts/contract"
317
-
318
- // Custom error type
319
- export class InvalidCredentialsError extends BAD_REQUEST<"INVALID_CREDENTIALS", undefined> {
320
- public static TYPE = "INVALID_CREDENTIALS"
321
-
322
- constructor() {
323
- super("INVALID_CREDENTIALS", undefined)
324
- }
325
- }
326
-
327
- export const MyApiContract = defineApi("MyApi", () => ({
328
- get: {
329
- input: IsItemIdParam,
330
- success: IsItem,
331
- errors: [NOT_FOUND, SERVER_ERROR]
332
- },
333
- update: {
334
- input: IsUpdateRequest,
335
- success: IsItem,
336
- errors: [NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR]
337
- },
338
- login: {
339
- input: IsLoginRequest,
340
- success: IsLoginResponse,
341
- errors: [InvalidCredentialsError, VALIDATION_ERROR, SERVER_ERROR]
342
- }
343
- }))
344
- ```
345
-
346
- ### Throwing Errors in Service
347
-
348
- ```typescript
349
- import { GGServerApi, NOT_FOUND, FORBIDDEN } from "@grest-ts/node"
350
-
351
- export class MyService implements GGServerApi<typeof MyApiContract["methods"]> {
352
- async get({ id }: { id: tItemId }): Promise<Item> {
353
- const item = await this.findItem(id)
354
- if (!item) throw new NOT_FOUND()
355
- return item
356
- }
357
-
358
- async update(request: UpdateRequest): Promise<Item> {
359
- const item = await this.findItem(request.id)
360
- if (!item) throw new NOT_FOUND()
361
-
362
- const user = UserContext.get()
363
- if (item.ownerId !== user.id) throw new FORBIDDEN()
364
-
365
- return this.updateItem(item, request)
366
- }
367
- }
368
- ```
369
-
370
- ## HTTP Server Setup
371
-
372
- ### Using GGHttp (Fluent API)
373
-
374
- ```typescript
375
- import { GGHttp } from "@grest-ts/http"
376
-
377
- protected compose(): void {
378
- new GGHttp()
379
- .http(PublicApi, publicService)
380
- .http(StatusApi, {
381
- status: async () => ({ status: true })
382
- })
383
-
384
- new GGHttp("authenticated")
385
- .use(new UserContextMiddleware(userService))
386
- .http(MyApi, myService)
387
- .http(UserAuthApi, userService)
388
- .websocket(NotificationApi, notificationService.handleConnection)
389
- }
390
- ```
391
-
392
- ### Using GGHttpServer (Direct)
393
-
394
- ```typescript
395
- import { GGHttpServer } from "@grest-ts/http"
396
-
397
- protected compose(): void {
398
- const httpServer = new GGHttpServer()
399
- MyApi.startServer(httpServer, myService)
400
- OtherApi.startServer(httpServer, otherService)
401
- }
402
- ```
403
-
404
- ### Multiple HTTP Servers
405
-
406
- ```typescript
407
- protected compose(): void {
408
- // Main public server
409
- new GGHttp()
410
- .http(PublicApi, publicService)
411
-
412
- // Internal server on different port
413
- new GGHttp("internal")
414
- .http(InternalApi, internalService)
415
- }
416
- ```
417
-
418
- ## HTTP Client
419
-
420
- ### Creating Clients
421
-
422
- ```typescript
423
- // Unauthenticated client
424
- const client = MyApi.createClient({ url: "http://localhost:3000" })
425
-
426
- // Authenticated client
427
- const authClient = MyApi.createClient(authState, { url: "http://localhost:3000" })
428
-
429
- // Test client
430
- const testClient = MyApi.createTestClient()
431
- ```
432
-
433
- ### Making Requests
434
-
435
- ```typescript
436
- // Simple request
437
- const items = await client.list()
438
-
439
- // With path parameter
440
- const item = await client.get({ id: "item-123" })
441
-
442
- // With query parameters
443
- const results = await client.search({ term: "foo", page: 1 })
444
-
445
- // With body
446
- const newItem = await client.create({ title: "New Item" })
447
- ```
448
-
449
- ### Handling Results
450
-
451
- ```typescript
452
- // Direct (throws on error)
453
- const item = await client.get({ id: "item-123" })
454
-
455
- // Using .asResult() for safe error handling
456
- const result = await client.get({ id: "item-123" }).asResult()
457
- if (result.success) {
458
- console.log("Item:", result.data)
459
- } else {
460
- console.log("Error:", result.type) // "NOT_FOUND", etc.
461
- }
462
- ```
463
-
464
- ## WebSocket APIs
465
-
466
- ### Defining WebSocket API
467
-
468
- ```typescript
469
- import { webSocketApi } from "@grest-ts/http"
470
- import { defineTwoWayApi, SERVER_ERROR, VALIDATION_ERROR } from "@grest-ts/contract"
471
- import { IsObject, IsString, IsBoolean } from "@grest-ts/schema"
472
- import { GG_USER_AUTH_TOKEN } from "./auth/UserAuth"
473
-
474
- // Message schemas
475
- export const IsItemMarkedEvent = IsObject({
476
- item: IsItem,
477
- markedBy: IsString
478
- })
479
- export type ItemMarkedEvent = typeof IsItemMarkedEvent.infer
480
-
481
- export const IsUpdateItemRequest = IsObject({
482
- item: IsItem,
483
- reason: IsString.orUndefined
484
- })
485
-
486
- export const IsUpdateItemResponse = IsObject({
487
- success: IsBoolean,
488
- message: IsString
489
- })
490
-
491
- // Contract definition
492
- export const NotificationApiContract = defineTwoWayApi("NotificationApi", () => ({
493
- clientToServer: {
494
- updateItem: {
495
- input: IsUpdateItemRequest,
496
- success: IsUpdateItemResponse,
497
- errors: [VALIDATION_ERROR, SERVER_ERROR]
498
- },
499
- ping: {}
500
- },
501
- serverToClient: {
502
- itemMarked: {
503
- input: IsItemMarkedEvent
504
- },
505
- areYouThere: {
506
- success: IsBoolean,
507
- errors: [SERVER_ERROR]
508
- }
509
- }
510
- }))
511
-
512
- export const NotificationApi = webSocketApi(NotificationApiContract)
513
- .path("ws/notifications")
514
- .use(GG_USER_AUTH_TOKEN)
515
- .done()
516
- ```
517
-
518
- ### WebSocket Server Handler
519
-
520
- ```typescript
521
- import { GGSocketApi, WebSocketIncoming, WebSocketOutgoing } from "@grest-ts/http"
522
-
523
- type IncomingHandler = WebSocketIncoming<GGSocketApi<typeof NotificationApiContract.methods["clientToServer"]>>
524
- type OutgoingConnection = WebSocketOutgoing<GGSocketApi<typeof NotificationApiContract.methods["serverToClient"]>>
525
-
526
- export class NotificationService {
527
- private connections = new Map<string, Set<OutgoingConnection>>()
528
-
529
- handleConnection = (incoming: IncomingHandler, outgoing: OutgoingConnection): void => {
530
- const user = UserContext.get()
531
-
532
- // Track connection
533
- if (!this.connections.has(user.id)) {
534
- this.connections.set(user.id, new Set())
535
- }
536
- this.connections.get(user.id)!.add(outgoing)
537
-
538
- // Handle incoming messages
539
- incoming.on({
540
- updateItem: async (request) => {
541
- // Process update
542
- return { success: true, message: "Updated" }
543
- },
544
- ping: async () => {
545
- // Handle ping
546
- }
547
- })
548
-
549
- // Handle disconnect
550
- outgoing.onClose(() => {
551
- this.connections.get(user.id)?.delete(outgoing)
552
- })
553
- }
554
-
555
- // Broadcast to user
556
- notifyUser(userId: string, event: ItemMarkedEvent): void {
557
- const userConnections = this.connections.get(userId)
558
- userConnections?.forEach(conn => conn.itemMarked(event))
559
- }
560
- }
561
- ```
562
-
563
- ### WebSocket in Runtime
564
-
565
- ```typescript
566
- protected compose(): void {
567
- new GGHttp()
568
- .use(new UserContextMiddleware(userService))
569
- .websocket(NotificationApi, notificationService.handleConnection)
570
- }
571
- ```
572
-
573
- ### WebSocket Client (Browser)
574
-
575
- ```typescript
576
- // Connect with message handlers
577
- const socket = await authenticatedSDK.connectNotification({
578
- itemMarked: (event) => {
579
- console.log("Item marked:", event.item.title)
580
- },
581
- areYouThere: async () => {
582
- return true
583
- }
584
- })
585
-
586
- // Send messages
587
- const response = await socket.updateItem({ item, reason: "Updated via UI" })
588
-
589
- // Close connection
590
- socket.close()
591
- ```
592
-
593
- ## SDK (Auto-Generated)
594
-
595
- The SDK is auto-generated from your API definitions. The generated SDK provides:
596
-
597
- - Type-safe client methods for all endpoints
598
- - Automatic auth token handling
599
- - WebSocket connection management
600
- - Error type inference
601
-
602
- ### Using Generated SDK
603
-
604
- ```typescript
605
- import { UserAppSDK } from "./UserAppSDK.gen"
606
-
607
- const sdk = new UserAppSDK({ url: "http://localhost:3000" })
608
-
609
- // Public endpoints
610
- const loginResult = await sdk.login({ username, password })
611
-
612
- // Authenticated endpoints (returned from login)
613
- const authSDK = loginResult.data.sdk
614
- const items = await authSDK.checklist.list()
615
- const socket = await authSDK.connectNotification({
616
- itemMarked: (event) => console.log(event)
617
- })
618
- ```
6
+ # HTTP Package Usage (@grest-ts/http)
7
+
8
+ How to use the HTTP package for building type-safe HTTP and WebSocket APIs.
9
+
10
+ ## HTTP API Definition
11
+
12
+ ### Basic API Structure
13
+
14
+ ```typescript
15
+ // MyApi.ts
16
+ import { GGRpc, httpApi } from "@grest-ts/http"
17
+ import { IsArray, IsObject, IsString, IsBoolean, IsUint } from "@grest-ts/schema"
18
+ import { defineApi, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR } from "@grest-ts/contract"
19
+
20
+ // ---------------------------------------------------------
21
+ // Type Schemas
22
+ // ---------------------------------------------------------
23
+
24
+ export const IsItemId = IsString.brand("ItemId")
25
+ export type tItemId = typeof IsItemId.infer
26
+
27
+ export const IsItem = IsObject({
28
+ id: IsItemId,
29
+ title: IsString,
30
+ description: IsString.orUndefined,
31
+ done: IsBoolean,
32
+ createdAt: IsUint,
33
+ updatedAt: IsUint
34
+ })
35
+ export type Item = typeof IsItem.infer
36
+
37
+ export const IsCreateItemRequest = IsObject({
38
+ title: IsString.nonEmpty,
39
+ description: IsString.orUndefined
40
+ })
41
+ export type CreateItemRequest = typeof IsCreateItemRequest.infer
42
+
43
+ export const IsUpdateItemRequest = IsObject({
44
+ id: IsItemId,
45
+ title: IsString.orUndefined,
46
+ description: IsString.orUndefined
47
+ })
48
+ export type UpdateItemRequest = typeof IsUpdateItemRequest.infer
49
+
50
+ export const IsItemIdParam = IsObject({
51
+ id: IsItemId
52
+ })
53
+
54
+ // ---------------------------------------------------------
55
+ // Contract & API
56
+ // ---------------------------------------------------------
57
+
58
+ export const MyApiContract = defineApi("MyApi", () => ({
59
+ list: {
60
+ success: IsArray(IsItem),
61
+ errors: [SERVER_ERROR]
62
+ },
63
+ get: {
64
+ input: IsItemIdParam,
65
+ success: IsItem,
66
+ errors: [NOT_FOUND, SERVER_ERROR]
67
+ },
68
+ create: {
69
+ input: IsCreateItemRequest,
70
+ success: IsItem,
71
+ errors: [VALIDATION_ERROR, SERVER_ERROR]
72
+ },
73
+ update: {
74
+ input: IsUpdateItemRequest,
75
+ success: IsItem,
76
+ errors: [NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR]
77
+ },
78
+ delete: {
79
+ input: IsItemIdParam,
80
+ success: undefined as undefined,
81
+ errors: [NOT_FOUND, SERVER_ERROR]
82
+ }
83
+ }))
84
+
85
+ export const MyApi = httpApi(MyApiContract)
86
+ .pathPrefix("api/items")
87
+ .routes({
88
+ list: GGRpc.GET("list"),
89
+ get: GGRpc.GET("get/:id"),
90
+ create: GGRpc.POST("create"),
91
+ update: GGRpc.PUT("update"),
92
+ delete: GGRpc.DELETE("delete/:id")
93
+ })
94
+ ```
95
+
96
+ ### HTTP Methods
97
+
98
+ ```typescript
99
+ GGRpc.GET("path") // GET request
100
+ GGRpc.POST("path") // POST request
101
+ GGRpc.PUT("path") // PUT request
102
+ GGRpc.PATCH("path") // PATCH request
103
+ GGRpc.DELETE("path") // DELETE request
104
+ ```
105
+
106
+ ### Path Parameters
107
+
108
+ Use `:paramName` in paths - parameters are matched by position:
109
+
110
+ ```typescript
111
+ export const MyApiContract = defineApi("MyApi", () => ({
112
+ getUser: {
113
+ input: IsObject({ userId: IsUserId }),
114
+ success: IsUser,
115
+ errors: [NOT_FOUND, SERVER_ERROR]
116
+ },
117
+ getUserPost: {
118
+ input: IsObject({ userId: IsUserId, postId: IsPostId }),
119
+ success: IsPost,
120
+ errors: [NOT_FOUND, SERVER_ERROR]
121
+ }
122
+ }))
123
+
124
+ export const MyApi = httpApi(MyApiContract)
125
+ .pathPrefix("api")
126
+ .routes({
127
+ getUser: GGRpc.GET("users/:userId"),
128
+ getUserPost: GGRpc.GET("users/:userId/posts/:postId")
129
+ })
130
+ ```
131
+
132
+ ### Query Parameters
133
+
134
+ For GET/DELETE, object parameters become query strings:
135
+
136
+ ```typescript
137
+ export const MyApiContract = defineApi("MyApi", () => ({
138
+ search: {
139
+ input: IsObject({
140
+ term: IsString,
141
+ page: IsUint.orUndefined,
142
+ limit: IsUint.orUndefined
143
+ }),
144
+ success: IsSearchResults,
145
+ errors: [SERVER_ERROR]
146
+ }
147
+ }))
148
+
149
+ // Client usage: client.search({ term: "foo", page: 1 })
150
+ // Results in: GET /api/search?term=foo&page=1
151
+ ```
152
+
153
+ ### Request Body
154
+
155
+ For POST/PUT/PATCH, the input becomes the JSON body:
156
+
157
+ ```typescript
158
+ export const MyApiContract = defineApi("MyApi", () => ({
159
+ create: {
160
+ input: IsCreateRequest,
161
+ success: IsItem,
162
+ errors: [VALIDATION_ERROR, SERVER_ERROR]
163
+ },
164
+ update: {
165
+ input: IsUpdateRequest,
166
+ success: IsItem,
167
+ errors: [VALIDATION_ERROR, SERVER_ERROR]
168
+ }
169
+ }))
170
+ ```
171
+
172
+ ## Authentication & Context
173
+
174
+ ### Using Codec (Recommended for Header-Based Auth)
175
+
176
+ ```typescript
177
+ // auth/UserAuth.ts
178
+ import { GGContextKey } from "@grest-ts/context"
179
+ import { IsObject, IsString } from "@grest-ts/schema"
180
+
181
+ export const IsUserAuthToken = IsString.brand("UserAuthToken")
182
+ export type tUserAuthToken = typeof IsUserAuthToken.infer
183
+
184
+ export const IsUserId = IsString.brand("UserId")
185
+ export type tUserId = typeof IsUserId.infer
186
+
187
+ export const IsUser = IsObject({
188
+ id: IsUserId,
189
+ username: IsString,
190
+ email: IsString
191
+ })
192
+ export type User = typeof IsUser.infer
193
+
194
+ // Define the context value schema
195
+ const IsUserAuthContext = IsObject({
196
+ token: IsUserAuthToken
197
+ })
198
+ export type UserAuthContext = typeof IsUserAuthContext.infer
199
+
200
+ // Define the header schema
201
+ const HEADER_AUTHORIZATION = "authorization"
202
+ const HeaderType = IsObject({
203
+ [HEADER_AUTHORIZATION]: IsString.orUndefined
204
+ })
205
+
206
+ // Create context key with codec
207
+ export const GG_USER_AUTH = new GGContextKey<UserAuthContext>("user_auth", IsUserAuthContext)
208
+ GG_USER_AUTH.addCodec("http", HeaderType.codecTo(IsUserAuthContext, {
209
+ encode: (headers) => {
210
+ const authHeader = headers[HEADER_AUTHORIZATION]
211
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
212
+ return { token: undefined as any } // Will fail validation if required
213
+ }
214
+ return { token: authHeader.substring(7) as tUserAuthToken }
215
+ },
216
+ decode: (value) => {
217
+ return { [HEADER_AUTHORIZATION]: value.token ? `Bearer ${value.token}` : undefined }
218
+ }
219
+ }))
220
+ ```
221
+
222
+ ### Using Middleware (For Complex Logic)
223
+
224
+ ```typescript
225
+ // middleware/ClientInfoMiddleware.ts
226
+ import { GGHttpRequest, GGHttpTransportMiddleware } from "@grest-ts/http"
227
+ import { GGContextKey } from "@grest-ts/context"
228
+ import { IsObject, IsString, IsLiteral } from "@grest-ts/schema"
229
+
230
+ export interface ClientInfo {
231
+ version: string
232
+ platform: 'web' | 'ios' | 'android'
233
+ }
234
+
235
+ export const GG_CLIENT_INFO = new GGContextKey<ClientInfo>('clientInfo', IsObject({
236
+ version: IsString,
237
+ platform: IsLiteral("web", "ios", "android")
238
+ }))
239
+
240
+ export const ClientInfoMiddleware: GGHttpTransportMiddleware = {
241
+ updateRequest(req: GGHttpRequest): void {
242
+ const info = GG_CLIENT_INFO.get()
243
+ if (info) {
244
+ req.headers['x-client-version'] = info.version
245
+ req.headers['x-client-platform'] = info.platform
246
+ }
247
+ },
248
+ parseRequest(req: GGHttpRequest): void {
249
+ GG_CLIENT_INFO.set({
250
+ version: req.headers['x-client-version'] ?? 'unknown',
251
+ platform: (req.headers['x-client-platform'] ?? 'web') as ClientInfo['platform']
252
+ })
253
+ }
254
+ }
255
+ ```
256
+
257
+ ### Adding Auth/Context to API
258
+
259
+ ```typescript
260
+ import { GG_USER_AUTH } from "./auth/UserAuth"
261
+ import { ClientInfoMiddleware } from "./middleware/ClientInfoMiddleware"
262
+ import { GG_INTL_LOCALE } from "@grest-ts/intl"
263
+
264
+ export const MyApiContract = defineApi("MyApi", () => ({
265
+ list: {
266
+ success: IsArray(IsItem),
267
+ errors: [NOT_AUTHORIZED, SERVER_ERROR]
268
+ },
269
+ create: {
270
+ input: IsCreateRequest,
271
+ success: IsItem,
272
+ errors: [NOT_AUTHORIZED, VALIDATION_ERROR, SERVER_ERROR]
273
+ }
274
+ }))
275
+
276
+ // Chain multiple context providers
277
+ export const MyApi = httpApi(MyApiContract)
278
+ .pathPrefix("api/items")
279
+ .useHeader(GG_INTL_LOCALE) // Use codec from context key
280
+ .useHeader(GG_USER_AUTH) // Use codec from context key
281
+ .use(ClientInfoMiddleware) // Use middleware object
282
+ .routes({
283
+ list: GGRpc.GET("list"),
284
+ create: GGRpc.POST("create")
285
+ })
286
+ ```
287
+
288
+ ### Public API (No Auth)
289
+
290
+ ```typescript
291
+ export const PublicApiContract = defineApi("PublicApi", () => ({
292
+ status: {
293
+ success: IsStatusResponse,
294
+ errors: [SERVER_ERROR]
295
+ },
296
+ login: {
297
+ input: IsLoginRequest,
298
+ success: IsLoginResponse,
299
+ errors: [VALIDATION_ERROR, SERVER_ERROR]
300
+ }
301
+ }))
302
+
303
+ export const PublicApi = httpApi(PublicApiContract)
304
+ .pathPrefix("pub")
305
+ .routes({
306
+ status: GGRpc.GET("status"),
307
+ login: GGRpc.POST("login")
308
+ })
309
+ ```
310
+
311
+ ## Error Types
312
+
313
+ ### Declaring Errors in Contract
314
+
315
+ ```typescript
316
+ import { defineApi, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR, BAD_REQUEST } from "@grest-ts/contract"
317
+
318
+ // Custom error type
319
+ export class InvalidCredentialsError extends BAD_REQUEST<"INVALID_CREDENTIALS", undefined> {
320
+ public static TYPE = "INVALID_CREDENTIALS"
321
+
322
+ constructor() {
323
+ super("INVALID_CREDENTIALS", undefined)
324
+ }
325
+ }
326
+
327
+ export const MyApiContract = defineApi("MyApi", () => ({
328
+ get: {
329
+ input: IsItemIdParam,
330
+ success: IsItem,
331
+ errors: [NOT_FOUND, SERVER_ERROR]
332
+ },
333
+ update: {
334
+ input: IsUpdateRequest,
335
+ success: IsItem,
336
+ errors: [NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR]
337
+ },
338
+ login: {
339
+ input: IsLoginRequest,
340
+ success: IsLoginResponse,
341
+ errors: [InvalidCredentialsError, VALIDATION_ERROR, SERVER_ERROR]
342
+ }
343
+ }))
344
+ ```
345
+
346
+ ### Throwing Errors in Service
347
+
348
+ ```typescript
349
+ import { GGServerApi, NOT_FOUND, FORBIDDEN } from "@grest-ts/node"
350
+
351
+ export class MyService implements GGServerApi<typeof MyApiContract["methods"]> {
352
+ async get({ id }: { id: tItemId }): Promise<Item> {
353
+ const item = await this.findItem(id)
354
+ if (!item) throw new NOT_FOUND()
355
+ return item
356
+ }
357
+
358
+ async update(request: UpdateRequest): Promise<Item> {
359
+ const item = await this.findItem(request.id)
360
+ if (!item) throw new NOT_FOUND()
361
+
362
+ const user = UserContext.get()
363
+ if (item.ownerId !== user.id) throw new FORBIDDEN()
364
+
365
+ return this.updateItem(item, request)
366
+ }
367
+ }
368
+ ```
369
+
370
+ ## HTTP Server Setup
371
+
372
+ ### Using GGHttp (Fluent API)
373
+
374
+ ```typescript
375
+ import { GGHttp } from "@grest-ts/http"
376
+
377
+ protected compose(): void {
378
+ new GGHttp()
379
+ .http(PublicApi, publicService)
380
+ .http(StatusApi, {
381
+ status: async () => ({ status: true })
382
+ })
383
+
384
+ new GGHttp("authenticated")
385
+ .use(new UserContextMiddleware(userService))
386
+ .http(MyApi, myService)
387
+ .http(UserAuthApi, userService)
388
+ .websocket(NotificationApi, notificationService.handleConnection)
389
+ }
390
+ ```
391
+
392
+ ### Using GGHttpServer (Direct)
393
+
394
+ ```typescript
395
+ import { GGHttpServer } from "@grest-ts/http"
396
+
397
+ protected compose(): void {
398
+ const httpServer = new GGHttpServer()
399
+ MyApi.startServer(httpServer, myService)
400
+ OtherApi.startServer(httpServer, otherService)
401
+ }
402
+ ```
403
+
404
+ ### Multiple HTTP Servers
405
+
406
+ ```typescript
407
+ protected compose(): void {
408
+ // Main public server
409
+ new GGHttp()
410
+ .http(PublicApi, publicService)
411
+
412
+ // Internal server on different port
413
+ new GGHttp("internal")
414
+ .http(InternalApi, internalService)
415
+ }
416
+ ```
417
+
418
+ ## HTTP Client
419
+
420
+ ### Creating Clients
421
+
422
+ ```typescript
423
+ // Unauthenticated client
424
+ const client = MyApi.createClient({ url: "http://localhost:3000" })
425
+
426
+ // Authenticated client
427
+ const authClient = MyApi.createClient(authState, { url: "http://localhost:3000" })
428
+
429
+ // Test client
430
+ const testClient = MyApi.createTestClient()
431
+ ```
432
+
433
+ ### Making Requests
434
+
435
+ ```typescript
436
+ // Simple request
437
+ const items = await client.list()
438
+
439
+ // With path parameter
440
+ const item = await client.get({ id: "item-123" })
441
+
442
+ // With query parameters
443
+ const results = await client.search({ term: "foo", page: 1 })
444
+
445
+ // With body
446
+ const newItem = await client.create({ title: "New Item" })
447
+ ```
448
+
449
+ ### Handling Results
450
+
451
+ ```typescript
452
+ // Direct (throws on error)
453
+ const item = await client.get({ id: "item-123" })
454
+
455
+ // Using .asResult() for safe error handling
456
+ const result = await client.get({ id: "item-123" }).asResult()
457
+ if (result.success) {
458
+ console.log("Item:", result.data)
459
+ } else {
460
+ console.log("Error:", result.type) // "NOT_FOUND", etc.
461
+ }
462
+ ```
463
+
464
+ ## WebSocket APIs
465
+
466
+ ### Defining WebSocket API
467
+
468
+ ```typescript
469
+ import { webSocketApi } from "@grest-ts/http"
470
+ import { defineTwoWayApi, SERVER_ERROR, VALIDATION_ERROR } from "@grest-ts/contract"
471
+ import { IsObject, IsString, IsBoolean } from "@grest-ts/schema"
472
+ import { GG_USER_AUTH_TOKEN } from "./auth/UserAuth"
473
+
474
+ // Message schemas
475
+ export const IsItemMarkedEvent = IsObject({
476
+ item: IsItem,
477
+ markedBy: IsString
478
+ })
479
+ export type ItemMarkedEvent = typeof IsItemMarkedEvent.infer
480
+
481
+ export const IsUpdateItemRequest = IsObject({
482
+ item: IsItem,
483
+ reason: IsString.orUndefined
484
+ })
485
+
486
+ export const IsUpdateItemResponse = IsObject({
487
+ success: IsBoolean,
488
+ message: IsString
489
+ })
490
+
491
+ // Contract definition
492
+ export const NotificationApiContract = defineTwoWayApi("NotificationApi", () => ({
493
+ clientToServer: {
494
+ updateItem: {
495
+ input: IsUpdateItemRequest,
496
+ success: IsUpdateItemResponse,
497
+ errors: [VALIDATION_ERROR, SERVER_ERROR]
498
+ },
499
+ ping: {}
500
+ },
501
+ serverToClient: {
502
+ itemMarked: {
503
+ input: IsItemMarkedEvent
504
+ },
505
+ areYouThere: {
506
+ success: IsBoolean,
507
+ errors: [SERVER_ERROR]
508
+ }
509
+ }
510
+ }))
511
+
512
+ export const NotificationApi = webSocketApi(NotificationApiContract)
513
+ .path("ws/notifications")
514
+ .use(GG_USER_AUTH_TOKEN)
515
+ .done()
516
+ ```
517
+
518
+ ### WebSocket Server Handler
519
+
520
+ ```typescript
521
+ import { GGSocketApi, WebSocketIncoming, WebSocketOutgoing } from "@grest-ts/http"
522
+
523
+ type IncomingHandler = WebSocketIncoming<GGSocketApi<typeof NotificationApiContract.methods["clientToServer"]>>
524
+ type OutgoingConnection = WebSocketOutgoing<GGSocketApi<typeof NotificationApiContract.methods["serverToClient"]>>
525
+
526
+ export class NotificationService {
527
+ private connections = new Map<string, Set<OutgoingConnection>>()
528
+
529
+ handleConnection = (incoming: IncomingHandler, outgoing: OutgoingConnection): void => {
530
+ const user = UserContext.get()
531
+
532
+ // Track connection
533
+ if (!this.connections.has(user.id)) {
534
+ this.connections.set(user.id, new Set())
535
+ }
536
+ this.connections.get(user.id)!.add(outgoing)
537
+
538
+ // Handle incoming messages
539
+ incoming.on({
540
+ updateItem: async (request) => {
541
+ // Process update
542
+ return { success: true, message: "Updated" }
543
+ },
544
+ ping: async () => {
545
+ // Handle ping
546
+ }
547
+ })
548
+
549
+ // Handle disconnect
550
+ outgoing.onClose(() => {
551
+ this.connections.get(user.id)?.delete(outgoing)
552
+ })
553
+ }
554
+
555
+ // Broadcast to user
556
+ notifyUser(userId: string, event: ItemMarkedEvent): void {
557
+ const userConnections = this.connections.get(userId)
558
+ userConnections?.forEach(conn => conn.itemMarked(event))
559
+ }
560
+ }
561
+ ```
562
+
563
+ ### WebSocket in Runtime
564
+
565
+ ```typescript
566
+ protected compose(): void {
567
+ new GGHttp()
568
+ .use(new UserContextMiddleware(userService))
569
+ .websocket(NotificationApi, notificationService.handleConnection)
570
+ }
571
+ ```
572
+
573
+ ### WebSocket Client (Browser)
574
+
575
+ ```typescript
576
+ // Connect with message handlers
577
+ const socket = await authenticatedSDK.connectNotification({
578
+ itemMarked: (event) => {
579
+ console.log("Item marked:", event.item.title)
580
+ },
581
+ areYouThere: async () => {
582
+ return true
583
+ }
584
+ })
585
+
586
+ // Send messages
587
+ const response = await socket.updateItem({ item, reason: "Updated via UI" })
588
+
589
+ // Close connection
590
+ socket.close()
591
+ ```
592
+
593
+ ## SDK (Auto-Generated)
594
+
595
+ The SDK is auto-generated from your API definitions. The generated SDK provides:
596
+
597
+ - Type-safe client methods for all endpoints
598
+ - Automatic auth token handling
599
+ - WebSocket connection management
600
+ - Error type inference
601
+
602
+ ### Using Generated SDK
603
+
604
+ ```typescript
605
+ import { UserAppSDK } from "./UserAppSDK.gen"
606
+
607
+ const sdk = new UserAppSDK({ url: "http://localhost:3000" })
608
+
609
+ // Public endpoints
610
+ const loginResult = await sdk.login({ username, password })
611
+
612
+ // Authenticated endpoints (returned from login)
613
+ const authSDK = loginResult.data.sdk
614
+ const items = await authSDK.checklist.list()
615
+ const socket = await authSDK.connectNotification({
616
+ itemMarked: (event) => console.log(event)
617
+ })
618
+ ```