@hypequery/serve 0.0.0-canary-20260306132943

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