@hypequery/serve 0.0.1

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.
Files changed (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1039 -0
  3. package/dist/adapters/fetch.d.ts +3 -0
  4. package/dist/adapters/fetch.d.ts.map +1 -0
  5. package/dist/adapters/fetch.js +26 -0
  6. package/dist/adapters/node.d.ts +8 -0
  7. package/dist/adapters/node.d.ts.map +1 -0
  8. package/dist/adapters/node.js +105 -0
  9. package/dist/adapters/utils.d.ts +39 -0
  10. package/dist/adapters/utils.d.ts.map +1 -0
  11. package/dist/adapters/utils.js +114 -0
  12. package/dist/adapters/vercel.d.ts +7 -0
  13. package/dist/adapters/vercel.d.ts.map +1 -0
  14. package/dist/adapters/vercel.js +13 -0
  15. package/dist/auth.d.ts +14 -0
  16. package/dist/auth.d.ts.map +1 -0
  17. package/dist/auth.js +37 -0
  18. package/dist/client-config.d.ts +44 -0
  19. package/dist/client-config.d.ts.map +1 -0
  20. package/dist/client-config.js +53 -0
  21. package/dist/dev.d.ts +9 -0
  22. package/dist/dev.d.ts.map +1 -0
  23. package/dist/dev.js +24 -0
  24. package/dist/docs-ui.d.ts +3 -0
  25. package/dist/docs-ui.d.ts.map +1 -0
  26. package/dist/docs-ui.js +34 -0
  27. package/dist/endpoint.d.ts +5 -0
  28. package/dist/endpoint.d.ts.map +1 -0
  29. package/dist/endpoint.js +58 -0
  30. package/dist/index.d.ts +13 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +12 -0
  33. package/dist/openapi.d.ts +3 -0
  34. package/dist/openapi.d.ts.map +1 -0
  35. package/dist/openapi.js +189 -0
  36. package/dist/queries.d.ts +3 -0
  37. package/dist/queries.d.ts.map +1 -0
  38. package/dist/queries.js +1 -0
  39. package/dist/query.d.ts +4 -0
  40. package/dist/query.d.ts.map +1 -0
  41. package/dist/query.js +1 -0
  42. package/dist/router.d.ts +13 -0
  43. package/dist/router.d.ts.map +1 -0
  44. package/dist/router.js +56 -0
  45. package/dist/sdk-generator.d.ts +7 -0
  46. package/dist/sdk-generator.d.ts.map +1 -0
  47. package/dist/sdk-generator.js +143 -0
  48. package/dist/server.d.ts +9 -0
  49. package/dist/server.d.ts.map +1 -0
  50. package/dist/server.js +580 -0
  51. package/dist/tenant.d.ts +35 -0
  52. package/dist/tenant.d.ts.map +1 -0
  53. package/dist/tenant.js +49 -0
  54. package/dist/type-tests/builder.test-d.d.ts +13 -0
  55. package/dist/type-tests/builder.test-d.d.ts.map +1 -0
  56. package/dist/type-tests/builder.test-d.js +20 -0
  57. package/dist/types.d.ts +363 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +1 -0
  60. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,1039 @@
1
+ # @hypequery/serve
2
+
3
+ Declarative HTTP server for exposing hypequery analytics endpoints. Build type-safe REST APIs with automatic OpenAPI documentation, authentication, middleware, and multi-platform deployment support.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @hypequery/serve zod
9
+ ```
10
+
11
+ Peer dependencies: `zod@^3`, `tsx@^4` (optional, for dev server)
12
+
13
+ ## Quick Start
14
+
15
+ ```ts
16
+ // analytics/server.ts
17
+ import { defineServe } from '@hypequery/serve';
18
+ import { z } from 'zod';
19
+ import { db } from './database';
20
+
21
+ const api = defineServe({
22
+ queries: {
23
+ weeklyRevenue: {
24
+ inputSchema: z.object({ startDate: z.string() }),
25
+ outputSchema: z.object({ total: z.number() }),
26
+ query: async ({ input, ctx }) => {
27
+ const result = await db
28
+ .select({ total: sum(sales.amount) })
29
+ .from(sales)
30
+ .where(gte(sales.date, input.startDate));
31
+ return { total: result[0].total };
32
+ },
33
+ },
34
+ },
35
+ });
36
+
37
+ // Auto-register routes
38
+ api.route('/weeklyRevenue', api.queries.weeklyRevenue);
39
+
40
+ // Start server
41
+ await api.start({ port: 3000 });
42
+ ```
43
+
44
+ Your API is now running with:
45
+ - **Endpoint**: `GET http://localhost:3000/weeklyRevenue?startDate=2025-01-01`
46
+ - **Docs**: `http://localhost:3000/docs` (interactive Swagger UI)
47
+ - **OpenAPI**: `http://localhost:3000/openapi.json` (machine-readable schema)
48
+
49
+ ---
50
+
51
+ ## Core Concepts
52
+
53
+ ### 1. Server Builders
54
+
55
+ #### `defineServe<TContext, TAuth, TQueries>(config)`
56
+
57
+ Main entry point for creating a hypequery server. Returns a `ServeBuilder` with methods to configure routes, middleware, and start the server.
58
+
59
+ **Parameters:**
60
+
61
+ ```ts
62
+ interface ServeConfig<TContext, TAuth, TQueries> {
63
+ // Query definitions
64
+ queries?: TQueries;
65
+
66
+ // Base path for all routes
67
+ basePath?: string;
68
+
69
+ // Context factory (runs per-request to inject dependencies)
70
+ context?: TContext | ((opts: { request: ServeRequest; auth: TAuth | null }) => TContext | Promise<TContext>);
71
+
72
+ // Authentication strategies
73
+ auth?: AuthStrategy<TAuth> | AuthStrategy<TAuth>[];
74
+
75
+ // Global middlewares (run before every endpoint)
76
+ middlewares?: ServeMiddleware<any, any, TContext, TAuth>[];
77
+
78
+ // Multi-tenancy configuration
79
+ tenant?: TenantConfig<TAuth>;
80
+
81
+ // Lifecycle hooks
82
+ hooks?: ServeLifecycleHooks<TAuth>;
83
+
84
+ // OpenAPI configuration
85
+ openapi?: OpenApiOptions;
86
+
87
+ // Docs UI configuration
88
+ docs?: DocsOptions;
89
+ }
90
+ ```
91
+
92
+ **Returns:**
93
+
94
+ ```ts
95
+ interface ServeBuilder<TQueries, TContext, TAuth> {
96
+ queries: TQueries; // Registered query definitions
97
+ _routeConfig: Record<string, { method: HttpMethod }>; // Route-level HTTP method overrides
98
+
99
+ // Register a route
100
+ route(path: string, endpoint: ServeEndpoint, options?: RouteOptions): ServeBuilder;
101
+
102
+ // Add global middleware
103
+ use(middleware: ServeMiddleware): ServeBuilder;
104
+
105
+ // Add authentication strategy
106
+ useAuth(strategy: AuthStrategy<TAuth>): ServeBuilder;
107
+
108
+ // Execute query directly (bypasses HTTP)
109
+ execute<K extends keyof TQueries>(
110
+ key: K,
111
+ options?: { input?: any; context?: Partial<TContext>; request?: Partial<ServeRequest> }
112
+ ): Promise<QueryResult<TQueries[K]>>;
113
+
114
+ // Get toolkit description (for LLM integration)
115
+ describe(): ToolkitDescription;
116
+
117
+ // Raw HTTP handler (for custom adapters)
118
+ handler: ServeHandler;
119
+
120
+ // Start Node.js HTTP server
121
+ start(options?: StartServerOptions): Promise<{ server: Server; stop: () => Promise<void> }>;
122
+ }
123
+ ```
124
+
125
+ **Example:**
126
+
127
+ ```ts
128
+ const api = defineServe({
129
+ basePath: '/api',
130
+ context: async ({ request, auth }) => ({
131
+ db: createDatabaseConnection(),
132
+ userId: auth?.userId,
133
+ }),
134
+ auth: createBearerTokenStrategy({
135
+ validate: async (token) => {
136
+ const user = await verifyJWT(token);
137
+ return user ? { userId: user.id, role: user.role } : null;
138
+ },
139
+ }),
140
+ queries: {
141
+ getUser: {
142
+ inputSchema: z.object({ id: z.string() }),
143
+ outputSchema: z.object({ name: z.string(), email: z.string() }),
144
+ query: async ({ input, ctx }) => {
145
+ return ctx.db.query.users.findFirst({
146
+ where: eq(users.id, input.id),
147
+ });
148
+ },
149
+ },
150
+ },
151
+ });
152
+
153
+ api.route('/users/:id', api.queries.getUser, { method: 'GET' });
154
+
155
+ await api.start({ port: 4000 });
156
+ ```
157
+
158
+ ---
159
+
160
+ #### `initServe<TContext, TAuth>(options)`
161
+
162
+ Advanced pattern for defining reusable query builders with context type inference. Use this when you want to define context once and create multiple queries that share the same types.
163
+
164
+ **Parameters:**
165
+
166
+ ```ts
167
+ interface ServeInitializerOptions<TContext, TAuth> {
168
+ context: TContext | ((opts: { request: ServeRequest; auth: TAuth | null }) => TContext | Promise<TContext>);
169
+ basePath?: string;
170
+ auth?: AuthStrategy<TAuth> | AuthStrategy<TAuth>[];
171
+ middlewares?: ServeMiddleware<any, any, TContext, TAuth>[];
172
+ tenant?: TenantConfig<TAuth>;
173
+ hooks?: ServeLifecycleHooks<TAuth>;
174
+ openapi?: OpenApiOptions;
175
+ docs?: DocsOptions;
176
+ }
177
+ ```
178
+
179
+ **Returns:**
180
+
181
+ ```ts
182
+ interface ServeInitializer<TContext, TAuth> {
183
+ // Procedure builder (chainable query configuration)
184
+ procedure: QueryProcedureBuilder<TContext, TAuth>;
185
+
186
+ // Alias for procedure
187
+ query: QueryProcedureBuilder<TContext, TAuth>;
188
+
189
+ // Helper to group queries
190
+ queries<TQueries>(definitions: TQueries): TQueries;
191
+
192
+ // Define server with queries
193
+ define<TQueries>(config: { queries: TQueries }): ServeBuilder<TQueries, TContext, TAuth>;
194
+ }
195
+ ```
196
+
197
+ **Example:**
198
+
199
+ ```ts
200
+ // Define context once
201
+ const t = initServe({
202
+ context: async ({ auth }) => ({
203
+ db: createDatabase(),
204
+ userId: auth?.userId,
205
+ }),
206
+ auth: createApiKeyStrategy({
207
+ validate: async (key) => {
208
+ const user = await validateApiKey(key);
209
+ return user ? { userId: user.id } : null;
210
+ },
211
+ }),
212
+ });
213
+
214
+ // Create queries with inferred types
215
+ const queries = t.queries({
216
+ getRevenue: t.procedure
217
+ .input(z.object({ startDate: z.string() }))
218
+ .output(z.object({ total: z.number() }))
219
+ .query(async ({ input, ctx }) => {
220
+ // ctx is fully typed as { db, userId }
221
+ return ctx.db.query.sales.aggregate({ startDate: input.startDate });
222
+ }),
223
+
224
+ createSale: t.procedure
225
+ .input(z.object({ amount: z.number(), product: z.string() }))
226
+ .output(z.object({ id: z.string() }))
227
+ .method('POST')
228
+ .query(async ({ input, ctx }) => {
229
+ const [sale] = await ctx.db.insert(sales).values({
230
+ amount: input.amount,
231
+ product: input.product,
232
+ userId: ctx.userId,
233
+ });
234
+ return { id: sale.id };
235
+ }),
236
+ });
237
+
238
+ // Define server
239
+ const api = t.define({ queries });
240
+
241
+ api
242
+ .route('/revenue', api.queries.getRevenue)
243
+ .route('/sales', api.queries.createSale);
244
+
245
+ await api.start();
246
+ ```
247
+
248
+ ---
249
+
250
+ ### 2. HTTP Adapters
251
+
252
+ Adapters convert the framework-agnostic `ServeHandler` into platform-specific handlers.
253
+
254
+ #### `createNodeHandler(handler)`
255
+
256
+ Creates a Node.js HTTP handler for use with `http.createServer()` or Express.
257
+
258
+ ```ts
259
+ import { createNodeHandler } from '@hypequery/serve';
260
+ import { createServer } from 'http';
261
+
262
+ const nodeHandler = createNodeHandler(api.handler);
263
+ const server = createServer(nodeHandler);
264
+ server.listen(3000);
265
+ ```
266
+
267
+ ---
268
+
269
+ #### `createFetchHandler(handler)`
270
+
271
+ Creates a Web Fetch API handler for modern runtimes (Cloudflare Workers, Deno, Bun).
272
+
273
+ ```ts
274
+ import { createFetchHandler } from '@hypequery/serve';
275
+
276
+ const fetchHandler = createFetchHandler(api.handler);
277
+
278
+ // Cloudflare Workers
279
+ export default {
280
+ fetch: fetchHandler,
281
+ };
282
+
283
+ // Deno
284
+ Deno.serve(fetchHandler);
285
+
286
+ // Bun
287
+ Bun.serve({
288
+ fetch: fetchHandler,
289
+ port: 3000,
290
+ });
291
+ ```
292
+
293
+ ---
294
+
295
+ #### `createVercelEdgeHandler(handler)`
296
+
297
+ Creates a Vercel Edge Runtime handler (uses Fetch API).
298
+
299
+ ```ts
300
+ // pages/api/analytics.ts
301
+ import { createVercelEdgeHandler } from '@hypequery/serve';
302
+ import { api } from '@/analytics/server';
303
+
304
+ export const config = { runtime: 'edge' };
305
+ export default createVercelEdgeHandler(api.handler);
306
+ ```
307
+
308
+ ---
309
+
310
+ #### `createVercelNodeHandler(handler)`
311
+
312
+ Creates a Vercel Node.js handler (uses Node HTTP).
313
+
314
+ ```ts
315
+ // pages/api/analytics.ts
316
+ import { createVercelNodeHandler } from '@hypequery/serve';
317
+ import { api } from '@/analytics/server';
318
+
319
+ export default createVercelNodeHandler(api.handler);
320
+ ```
321
+
322
+ ---
323
+
324
+ ### 3. Authentication
325
+
326
+ #### `createApiKeyStrategy<TAuth>(options)`
327
+
328
+ Creates an authentication strategy that validates API keys from headers or query parameters.
329
+
330
+ **Parameters:**
331
+
332
+ ```ts
333
+ interface ApiKeyStrategyOptions<TAuth> {
334
+ header?: string; // Header name (default: "authorization")
335
+ queryParam?: string; // Query param name (optional)
336
+ validate: (key: string, request: ServeRequest) => Promise<TAuth | null> | TAuth | null;
337
+ }
338
+ ```
339
+
340
+ **Example:**
341
+
342
+ ```ts
343
+ import { createApiKeyStrategy } from '@hypequery/serve';
344
+
345
+ const apiKeyAuth = createApiKeyStrategy({
346
+ header: 'x-api-key',
347
+ queryParam: 'apiKey', // Allow ?apiKey=xxx for development
348
+ validate: async (key, request) => {
349
+ const user = await db.query.apiKeys.findFirst({
350
+ where: eq(apiKeys.key, key),
351
+ });
352
+
353
+ if (!user || user.revoked) return null;
354
+
355
+ return {
356
+ userId: user.userId,
357
+ scopes: user.scopes,
358
+ };
359
+ },
360
+ });
361
+
362
+ const api = defineServe({
363
+ auth: apiKeyAuth,
364
+ queries: { /* ... */ },
365
+ });
366
+ ```
367
+
368
+ **Usage:**
369
+
370
+ ```bash
371
+ # Header (preferred)
372
+ curl -H "x-api-key: sk_live_abc123" http://localhost:3000/revenue
373
+
374
+ # Query param (development only)
375
+ curl http://localhost:3000/revenue?apiKey=sk_live_abc123
376
+ ```
377
+
378
+ ---
379
+
380
+ #### `createBearerTokenStrategy<TAuth>(options)`
381
+
382
+ Creates an authentication strategy that validates Bearer tokens (JWT, OAuth).
383
+
384
+ **Parameters:**
385
+
386
+ ```ts
387
+ interface BearerTokenStrategyOptions<TAuth> {
388
+ header?: string; // Header name (default: "authorization")
389
+ prefix?: string; // Token prefix (default: "Bearer ")
390
+ validate: (token: string, request: ServeRequest) => Promise<TAuth | null> | TAuth | null;
391
+ }
392
+ ```
393
+
394
+ **Example:**
395
+
396
+ ```ts
397
+ import { createBearerTokenStrategy } from '@hypequery/serve';
398
+ import jwt from 'jsonwebtoken';
399
+
400
+ const jwtAuth = createBearerTokenStrategy({
401
+ validate: async (token) => {
402
+ try {
403
+ const payload = jwt.verify(token, process.env.JWT_SECRET) as {
404
+ sub: string;
405
+ role: string;
406
+ };
407
+
408
+ return {
409
+ userId: payload.sub,
410
+ role: payload.role,
411
+ };
412
+ } catch {
413
+ return null; // Invalid token
414
+ }
415
+ },
416
+ });
417
+
418
+ const api = defineServe({
419
+ auth: jwtAuth,
420
+ queries: { /* ... */ },
421
+ });
422
+ ```
423
+
424
+ **Usage:**
425
+
426
+ ```bash
427
+ curl -H "Authorization: Bearer eyJhbGc..." http://localhost:3000/revenue
428
+ ```
429
+
430
+ ---
431
+
432
+ ### 4. Procedure Builder (Advanced Query Configuration)
433
+
434
+ The procedure builder provides a chainable API for configuring queries with full type inference.
435
+
436
+ **Available Methods:**
437
+
438
+ ```ts
439
+ interface QueryProcedureBuilder<TContext, TAuth> {
440
+ // Input schema (Zod)
441
+ input<TSchema extends ZodTypeAny>(schema: TSchema): QueryProcedureBuilder;
442
+
443
+ // Output schema (Zod)
444
+ output<TSchema extends ZodTypeAny>(schema: TSchema): QueryProcedureBuilder;
445
+
446
+ // HTTP method (GET, POST, PUT, DELETE, etc.)
447
+ method(method: HttpMethod): QueryProcedureBuilder;
448
+
449
+ // Summary (OpenAPI short description)
450
+ summary(summary: string): QueryProcedureBuilder;
451
+
452
+ // Description (OpenAPI detailed description, supports Markdown)
453
+ describe(description: string): QueryProcedureBuilder;
454
+
455
+ // Add single tag (for OpenAPI grouping)
456
+ tag(tag: string): QueryProcedureBuilder;
457
+
458
+ // Add multiple tags
459
+ tags(tags: string[]): QueryProcedureBuilder;
460
+
461
+ // Cache TTL in milliseconds (sets Cache-Control header)
462
+ cache(ttlMs: number | null): QueryProcedureBuilder;
463
+
464
+ // Authentication strategy (overrides global auth)
465
+ auth(strategy: AuthStrategy<TAuth>): QueryProcedureBuilder;
466
+
467
+ // Multi-tenancy configuration
468
+ tenant(config: TenantConfig<TAuth>): QueryProcedureBuilder;
469
+
470
+ // Custom metadata (for extensions)
471
+ custom(metadata: Record<string, unknown>): QueryProcedureBuilder;
472
+
473
+ // Add middleware (runs before query handler)
474
+ use(...middlewares: ServeMiddleware[]): QueryProcedureBuilder;
475
+
476
+ // Define query handler (terminal operation)
477
+ query<TExecutable extends ExecutableQuery>(
478
+ executable: TExecutable
479
+ ): ServeQueryConfig;
480
+ }
481
+ ```
482
+
483
+ **Example:**
484
+
485
+ ```ts
486
+ const t = initServe({
487
+ context: async () => ({ db: createDatabase() }),
488
+ });
489
+
490
+ const getAnalytics = t.procedure
491
+ .input(z.object({
492
+ startDate: z.string(),
493
+ endDate: z.string(),
494
+ metric: z.enum(['revenue', 'users', 'sessions']),
495
+ }))
496
+ .output(z.object({
497
+ total: z.number(),
498
+ trend: z.number(),
499
+ }))
500
+ .method('GET')
501
+ .summary('Fetch analytics data')
502
+ .describe(`
503
+ Returns aggregated analytics for the specified metric and date range.
504
+ Supports revenue, user count, and session metrics.
505
+ `)
506
+ .tag('Analytics')
507
+ .cache(300000) // Cache for 5 minutes
508
+ .query(async ({ input, ctx }) => {
509
+ const result = await ctx.db.query.analytics.aggregate({
510
+ where: and(
511
+ gte(analytics.date, input.startDate),
512
+ lte(analytics.date, input.endDate),
513
+ ),
514
+ metric: input.metric,
515
+ });
516
+
517
+ return {
518
+ total: result.total,
519
+ trend: result.trend,
520
+ };
521
+ });
522
+ ```
523
+
524
+ ---
525
+
526
+ ### 5. Multi-Tenancy
527
+
528
+ Hypequery supports multi-tenant applications with automatic tenant isolation.
529
+
530
+ **Configuration:**
531
+
532
+ ```ts
533
+ interface TenantConfig<TAuth> {
534
+ // Extract tenant ID from auth context
535
+ extract: (auth: TAuth) => string | null;
536
+
537
+ // Tenant isolation mode
538
+ mode?: 'manual' | 'auto-inject'; // Default: 'manual'
539
+
540
+ // Column name for tenant filtering (required for auto-inject mode)
541
+ column?: string;
542
+
543
+ // Is tenant required? (default: true)
544
+ required?: boolean;
545
+
546
+ // Custom error message when tenant is missing
547
+ errorMessage?: string;
548
+ }
549
+ ```
550
+
551
+ **Example (Manual Mode):**
552
+
553
+ ```ts
554
+ const api = defineServe({
555
+ tenant: {
556
+ extract: (auth) => auth?.tenantId ?? null,
557
+ mode: 'manual', // You manually filter by tenantId
558
+ required: true,
559
+ },
560
+ context: async ({ auth }) => ({
561
+ db: createDatabase(),
562
+ tenantId: auth?.tenantId, // Injected by framework
563
+ }),
564
+ queries: {
565
+ getUsers: {
566
+ query: async ({ ctx }) => {
567
+ // Manually filter by tenant
568
+ return ctx.db.query.users.findMany({
569
+ where: eq(users.tenantId, ctx.tenantId),
570
+ });
571
+ },
572
+ },
573
+ },
574
+ });
575
+ ```
576
+
577
+ **Example (Auto-Inject Mode):**
578
+
579
+ ```ts
580
+ const api = defineServe({
581
+ tenant: {
582
+ extract: (auth) => auth?.organizationId ?? null,
583
+ mode: 'auto-inject',
584
+ column: 'organization_id', // Column to filter on
585
+ },
586
+ context: async () => ({
587
+ db: createDatabase(),
588
+ }),
589
+ queries: {
590
+ getUsers: {
591
+ query: async ({ ctx }) => {
592
+ // Tenant filter is automatically injected
593
+ return ctx.db.query.users.findMany();
594
+ // Equivalent to: WHERE organization_id = <tenant_id>
595
+ },
596
+ },
597
+ },
598
+ });
599
+ ```
600
+
601
+ ---
602
+
603
+ ### 6. Client Configuration
604
+
605
+ #### `extractClientConfig(api)`
606
+
607
+ Extracts serializable client configuration from a `ServeBuilder`. Returns HTTP method information for each query, used by `@hypequery/react` to configure hooks.
608
+
609
+ **Example:**
610
+
611
+ ```ts
612
+ // Server-side API route
613
+ import { api } from '@/analytics/server';
614
+ import { extractClientConfig } from '@hypequery/serve';
615
+
616
+ export async function GET() {
617
+ return Response.json(extractClientConfig(api));
618
+ }
619
+
620
+ // Returns:
621
+ // {
622
+ // "weeklyRevenue": { "method": "GET" },
623
+ // "createSale": { "method": "POST" }
624
+ // }
625
+ ```
626
+
627
+ **Client-side usage:**
628
+
629
+ ```ts
630
+ // lib/analytics.ts
631
+ import { createHooks } from '@hypequery/react';
632
+ import type { Api } from '@/analytics/server';
633
+
634
+ const config = await fetch('/api/config').then(r => r.json());
635
+
636
+ export const { useQuery, useMutation } = createHooks<Api>({
637
+ baseUrl: '/api/analytics',
638
+ config, // Auto-configures HTTP methods
639
+ });
640
+ ```
641
+
642
+ ---
643
+
644
+ #### `defineClientConfig(config)`
645
+
646
+ Type-safe helper to manually define client configuration when you can't access the API object.
647
+
648
+ **Example:**
649
+
650
+ ```ts
651
+ import { defineClientConfig } from '@hypequery/serve';
652
+
653
+ const config = defineClientConfig({
654
+ weeklyRevenue: { method: 'GET' },
655
+ createSale: { method: 'POST' },
656
+ updateProduct: { method: 'PUT' },
657
+ deleteOrder: { method: 'DELETE' },
658
+ });
659
+
660
+ export const { useQuery, useMutation } = createHooks<Api>({
661
+ baseUrl: '/api',
662
+ config,
663
+ });
664
+ ```
665
+
666
+ ---
667
+
668
+ ### 7. Development Server
669
+
670
+ #### `serveDev(api, options?)`
671
+
672
+ Starts a development server with enhanced logging and automatic documentation.
673
+
674
+ **Parameters:**
675
+
676
+ ```ts
677
+ interface ServeDevOptions {
678
+ port?: number; // Default: 4000 or process.env.PORT
679
+ hostname?: string; // Default: 'localhost'
680
+ quiet?: boolean; // Suppress logs (default: false)
681
+ signal?: AbortSignal; // Graceful shutdown signal
682
+ logger?: (message: string) => void; // Custom logger
683
+ }
684
+ ```
685
+
686
+ **Example:**
687
+
688
+ ```ts
689
+ import { serveDev } from '@hypequery/serve';
690
+ import { api } from './analytics/server';
691
+
692
+ await serveDev(api, {
693
+ port: 4000,
694
+ logger: (msg) => console.log(`[API] ${msg}`),
695
+ });
696
+
697
+ // Output:
698
+ // [API] hypequery dev server running at http://localhost:4000
699
+ // [API] Docs available at http://localhost:4000/docs
700
+ ```
701
+
702
+ ---
703
+
704
+ ## Advanced Features
705
+
706
+ ### Middleware
707
+
708
+ Middlewares run before query handlers and can modify context, validate permissions, or log requests.
709
+
710
+ **Signature:**
711
+
712
+ ```ts
713
+ type ServeMiddleware<TInput, TOutput, TContext, TAuth> = (
714
+ ctx: EndpointContext<TInput, TContext, TAuth>,
715
+ next: () => Promise<TOutput>
716
+ ) => Promise<TOutput>;
717
+ ```
718
+
719
+ **Example:**
720
+
721
+ ```ts
722
+ // Logging middleware
723
+ const logMiddleware: ServeMiddleware<any, any, any, any> = async (ctx, next) => {
724
+ console.log(`[${ctx.request.method}] ${ctx.request.path}`);
725
+ const start = Date.now();
726
+ const result = await next();
727
+ console.log(`Completed in ${Date.now() - start}ms`);
728
+ return result;
729
+ };
730
+
731
+ // Permission middleware
732
+ const requireAdmin: ServeMiddleware<any, any, any, { role: string }> = async (ctx, next) => {
733
+ if (ctx.auth?.role !== 'admin') {
734
+ throw new Error('Admin access required');
735
+ }
736
+ return next();
737
+ };
738
+
739
+ // Apply globally
740
+ const api = defineServe({
741
+ middlewares: [logMiddleware],
742
+ queries: { /* ... */ },
743
+ });
744
+
745
+ // Apply per-query
746
+ const deleteUser = t.procedure
747
+ .use(requireAdmin)
748
+ .query(async ({ input, ctx }) => {
749
+ // Only admins can reach here
750
+ });
751
+ ```
752
+
753
+ ---
754
+
755
+ ### Lifecycle Hooks
756
+
757
+ Hooks provide observability into the request lifecycle.
758
+
759
+ **Available Hooks:**
760
+
761
+ ```ts
762
+ interface ServeLifecycleHooks<TAuth> {
763
+ // Before request processing
764
+ onRequestStart?: (event: {
765
+ requestId: string;
766
+ queryKey: string;
767
+ metadata: EndpointMetadata;
768
+ request: ServeRequest;
769
+ auth: TAuth | null;
770
+ }) => void | Promise<void>;
771
+
772
+ // After successful request
773
+ onRequestEnd?: (event: {
774
+ requestId: string;
775
+ queryKey: string;
776
+ metadata: EndpointMetadata;
777
+ request: ServeRequest;
778
+ auth: TAuth | null;
779
+ durationMs: number;
780
+ result: unknown;
781
+ }) => void | Promise<void>;
782
+
783
+ // On authentication failure
784
+ onAuthFailure?: (event: {
785
+ requestId: string;
786
+ queryKey: string;
787
+ metadata: EndpointMetadata;
788
+ request: ServeRequest;
789
+ auth: TAuth | null;
790
+ reason: 'MISSING' | 'INVALID';
791
+ }) => void | Promise<void>;
792
+
793
+ // On any error
794
+ onError?: (event: {
795
+ requestId: string;
796
+ queryKey: string;
797
+ metadata: EndpointMetadata;
798
+ request: ServeRequest;
799
+ auth: TAuth | null;
800
+ durationMs: number;
801
+ error: unknown;
802
+ }) => void | Promise<void>;
803
+ }
804
+ ```
805
+
806
+ **Example:**
807
+
808
+ ```ts
809
+ const api = defineServe({
810
+ hooks: {
811
+ onRequestStart: async (event) => {
812
+ await analytics.track({
813
+ event: 'api_request_start',
814
+ queryKey: event.queryKey,
815
+ userId: event.auth?.userId,
816
+ });
817
+ },
818
+
819
+ onError: async (event) => {
820
+ await errorReporting.captureException(event.error, {
821
+ queryKey: event.queryKey,
822
+ requestId: event.requestId,
823
+ });
824
+ },
825
+ },
826
+ queries: { /* ... */ },
827
+ });
828
+ ```
829
+
830
+ ---
831
+
832
+ ### OpenAPI Configuration
833
+
834
+ **Options:**
835
+
836
+ ```ts
837
+ interface OpenApiOptions {
838
+ enabled?: boolean; // Enable OpenAPI endpoint (default: true)
839
+ path?: string; // OpenAPI JSON path (default: '/openapi.json')
840
+ info?: {
841
+ title?: string; // API title (default: 'Hypequery API')
842
+ version?: string; // API version (default: '1.0.0')
843
+ description?: string; // API description
844
+ };
845
+ servers?: Array<{
846
+ url: string;
847
+ description?: string;
848
+ }>;
849
+ }
850
+ ```
851
+
852
+ **Example:**
853
+
854
+ ```ts
855
+ const api = defineServe({
856
+ openapi: {
857
+ path: '/api-schema.json',
858
+ info: {
859
+ title: 'Analytics API',
860
+ version: '2.0.0',
861
+ description: 'Real-time analytics and reporting API',
862
+ },
863
+ servers: [
864
+ { url: 'https://api.example.com', description: 'Production' },
865
+ { url: 'http://localhost:4000', description: 'Development' },
866
+ ],
867
+ },
868
+ queries: { /* ... */ },
869
+ });
870
+ ```
871
+
872
+ ---
873
+
874
+ ### Documentation UI
875
+
876
+ **Options:**
877
+
878
+ ```ts
879
+ interface DocsOptions {
880
+ enabled?: boolean; // Enable docs UI (default: true)
881
+ path?: string; // Docs UI path (default: '/docs')
882
+ title?: string; // Page title (default: 'API Documentation')
883
+ }
884
+ ```
885
+
886
+ **Example:**
887
+
888
+ ```ts
889
+ const api = defineServe({
890
+ docs: {
891
+ path: '/api-docs',
892
+ title: 'Analytics API Reference',
893
+ },
894
+ queries: { /* ... */ },
895
+ });
896
+ ```
897
+
898
+ ---
899
+
900
+ ## Deployment Examples
901
+
902
+ ### Vercel (Edge Runtime)
903
+
904
+ ```ts
905
+ // pages/api/analytics/[...path].ts
906
+ import { createVercelEdgeHandler } from '@hypequery/serve';
907
+ import { api } from '@/analytics/server';
908
+
909
+ export const config = { runtime: 'edge' };
910
+ export default createVercelEdgeHandler(api.handler);
911
+ ```
912
+
913
+ ---
914
+
915
+ ### Cloudflare Workers
916
+
917
+ ```ts
918
+ // src/index.ts
919
+ import { createFetchHandler } from '@hypequery/serve';
920
+ import { api } from './analytics/server';
921
+
922
+ const handler = createFetchHandler(api.handler);
923
+
924
+ export default {
925
+ fetch: handler,
926
+ };
927
+ ```
928
+
929
+ ---
930
+
931
+ ### Express.js
932
+
933
+ ```ts
934
+ // server.ts
935
+ import express from 'express';
936
+ import { createNodeHandler } from '@hypequery/serve';
937
+ import { api } from './analytics/server';
938
+
939
+ const app = express();
940
+ const analyticsHandler = createNodeHandler(api.handler);
941
+
942
+ app.use('/api/analytics', analyticsHandler);
943
+ app.listen(3000);
944
+ ```
945
+
946
+ ---
947
+
948
+ ### Next.js App Router
949
+
950
+ ```ts
951
+ // app/api/analytics/[...path]/route.ts
952
+ import { createFetchHandler } from '@hypequery/serve';
953
+ import { api } from '@/analytics/server';
954
+
955
+ const handler = createFetchHandler(api.handler);
956
+
957
+ export { handler as GET, handler as POST, handler as PUT, handler as DELETE };
958
+ ```
959
+
960
+ ---
961
+
962
+ ## TypeScript
963
+
964
+ All functions are fully typed with automatic inference:
965
+
966
+ ```ts
967
+ import { defineServe } from '@hypequery/serve';
968
+ import { z } from 'zod';
969
+
970
+ const api = defineServe({
971
+ context: async ({ auth }) => ({
972
+ db: createDatabase(),
973
+ userId: auth?.userId,
974
+ }),
975
+ queries: {
976
+ getUser: {
977
+ inputSchema: z.object({ id: z.string() }),
978
+ outputSchema: z.object({ name: z.string(), email: z.string() }),
979
+ query: async ({ input, ctx }) => {
980
+ // input: { id: string }
981
+ // ctx: { db: Database; userId: string | undefined }
982
+ return ctx.db.query.users.findFirst({
983
+ where: eq(users.id, input.id),
984
+ });
985
+ },
986
+ },
987
+ },
988
+ });
989
+
990
+ // Execute with type safety
991
+ const user = await api.execute('getUser', {
992
+ input: { id: '123' }
993
+ });
994
+ // user: { name: string; email: string }
995
+ ```
996
+
997
+ ---
998
+
999
+ ## Error Handling
1000
+
1001
+ All errors follow a consistent format:
1002
+
1003
+ ```ts
1004
+ interface ErrorEnvelope {
1005
+ error: {
1006
+ type: 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'NOT_FOUND' | 'INTERNAL_SERVER_ERROR';
1007
+ message: string;
1008
+ details?: Record<string, unknown>;
1009
+ };
1010
+ }
1011
+ ```
1012
+
1013
+ **Example Error Response:**
1014
+
1015
+ ```json
1016
+ {
1017
+ "error": {
1018
+ "type": "VALIDATION_ERROR",
1019
+ "message": "Request validation failed",
1020
+ "details": {
1021
+ "issues": [
1022
+ {
1023
+ "code": "invalid_type",
1024
+ "expected": "string",
1025
+ "received": "number",
1026
+ "path": ["startDate"],
1027
+ "message": "Expected string, received number"
1028
+ }
1029
+ ]
1030
+ }
1031
+ }
1032
+ }
1033
+ ```
1034
+
1035
+ ---
1036
+
1037
+ ## License
1038
+
1039
+ Apache-2.0