@mateosuarezdev/brpc 1.0.50 → 1.0.52

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 ADDED
@@ -0,0 +1,701 @@
1
+ # brpc
2
+
3
+ A type-safe, batteries-included RPC framework for Bun. End-to-end type safety like tRPC, with built-in support for WebSocket subscriptions, file serving, media streaming, form uploads, server-side rendering, and more.
4
+
5
+ ```
6
+ npm install @mateosuarezdev/brpc zod
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ - [Quick Start](#quick-start)
14
+ - [Context](#context)
15
+ - [Procedures](#procedures)
16
+ - [Query](#query)
17
+ - [Mutation](#mutation)
18
+ - [FormMutation](#formmutation)
19
+ - [File](#file)
20
+ - [FileStream](#filestream)
21
+ - [HTML](#html)
22
+ - [Subscription](#subscription)
23
+ - [Middlewares](#middlewares)
24
+ - [createMiddleware](#createmiddleware)
25
+ - [Built-in Middlewares](#built-in-middlewares)
26
+ - [Router](#router)
27
+ - [Client](#client)
28
+ - [React](#react)
29
+ - [Schemas](#schemas)
30
+ - [Errors](#errors)
31
+ - [Streaming Media](#streaming-media)
32
+ - [Type Inference Utilities](#type-inference-utilities)
33
+
34
+ ---
35
+
36
+ ## Quick Start
37
+
38
+ ```ts
39
+ // context.ts
40
+ import { createContext } from "@mateosuarezdev/brpc";
41
+ import { db } from "./db";
42
+
43
+ export const context = createContext(async (req) => ({
44
+ db,
45
+ }));
46
+
47
+ // procedure.ts
48
+ import { createProcedure } from "@mateosuarezdev/brpc";
49
+ import { context } from "./context";
50
+
51
+ export const procedure = createProcedure(context);
52
+
53
+ // routes.ts
54
+ import { z } from "zod";
55
+ import { procedure } from "./procedure";
56
+
57
+ export const routes = {
58
+ hello: procedure
59
+ .input(z.object({ name: z.string() }))
60
+ .query(async ({ ctx, input }) => {
61
+ return `Hello, ${input.name}`;
62
+ }),
63
+ };
64
+
65
+ // index.ts
66
+ import { createRouter } from "@mateosuarezdev/brpc";
67
+ import { context } from "./context";
68
+ import { routes } from "./routes";
69
+
70
+ const router = createRouter({ context, routes });
71
+
72
+ router.listen(3000, () => {
73
+ console.log("Server running on http://localhost:3000");
74
+ });
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Context
80
+
81
+ `createContext` defines what data is available in every procedure handler. Return only your app-specific data — `req`, `params`, `headers`, and `publishToProcedure` are always injected automatically by the router.
82
+
83
+ ```ts
84
+ import { createContext } from "@mateosuarezdev/brpc";
85
+
86
+ export const context = createContext(async (req) => ({
87
+ db: getDb(),
88
+ userId: getUserIdFromCookie(req),
89
+ }));
90
+ ```
91
+
92
+ If you need no custom data at all:
93
+
94
+ ```ts
95
+ export const context = createContext(async (req) => {});
96
+ ```
97
+
98
+ Every handler receives a fully typed `ctx` with:
99
+
100
+ | Field | Type | Description |
101
+ |---|---|---|
102
+ | `req` | `Request` | The raw Bun request |
103
+ | `params` | `Record<string, string>` | URL path parameters |
104
+ | `headers` | `Headers` | Response headers you can set |
105
+ | `publishToProcedure` | `fn` | Publish to a subscription procedure |
106
+ | *your fields* | *inferred* | Whatever you return from `createContext` |
107
+
108
+ ---
109
+
110
+ ## Procedures
111
+
112
+ `createProcedure(context)` creates a typed procedure builder. Pass your context to infer the full context type automatically.
113
+
114
+ ```ts
115
+ import { createProcedure } from "@mateosuarezdev/brpc";
116
+ import { context } from "./context";
117
+
118
+ export const procedure = createProcedure(context);
119
+ ```
120
+
121
+ Chain `.use(middleware)` to add middleware, `.input(schema)` for input validation, then a handler method.
122
+
123
+ ### Query
124
+
125
+ GET request. Returns `{ data: result }`.
126
+
127
+ ```ts
128
+ const getUser = procedure
129
+ .input(z.object({ id: z.string() }))
130
+ .query(async ({ ctx, input }) => {
131
+ return ctx.db.users.findById(input.id);
132
+ });
133
+ ```
134
+
135
+ ### Mutation
136
+
137
+ POST request with a JSON body. Returns `{ data: result }`.
138
+
139
+ ```ts
140
+ const createPost = procedure
141
+ .input(z.object({ title: z.string(), body: z.string() }))
142
+ .mutation(async ({ ctx, input }) => {
143
+ return ctx.db.posts.create(input);
144
+ });
145
+ ```
146
+
147
+ ### FormMutation
148
+
149
+ POST with `multipart/form-data`. Useful for file uploads. Pair with `createFileSchema` for typed file validation.
150
+
151
+ ```ts
152
+ import { createFileSchema } from "@mateosuarezdev/brpc";
153
+
154
+ const uploadAvatar = procedure
155
+ .input(z.object({
156
+ file: createFileSchema({ acceptedTypes: { image: "*" }, maxSize: 5 }),
157
+ }))
158
+ .formMutation(async ({ ctx, input }) => {
159
+ await saveFile(input.file);
160
+ return { success: true };
161
+ });
162
+ ```
163
+
164
+ ### File
165
+
166
+ GET request that serves a file. brpc automatically sets appropriate `Content-Type` and `Cache-Control` headers.
167
+
168
+ ```ts
169
+ const logo = procedure.file(async ({ ctx }) => {
170
+ return Bun.file("./assets/logo.png");
171
+ });
172
+ ```
173
+
174
+ You can also return a `Response` directly for full control.
175
+
176
+ ### FileStream
177
+
178
+ GET request with HTTP Range support (206 Partial Content). Ideal for video and audio streaming.
179
+
180
+ ```ts
181
+ import { streamMedia } from "@mateosuarezdev/brpc";
182
+
183
+ const video = procedure.fileStream(async ({ ctx }) => {
184
+ return streamMedia(Bun.file("./videos/intro.mp4"), ctx.req, {
185
+ maxChunkSize: 2 * 1024 * 1024, // 2MB chunks
186
+ cacheMaxAge: 3600,
187
+ acceptedExtensions: [".mp4", ".webm"],
188
+ });
189
+ });
190
+ ```
191
+
192
+ ### HTML
193
+
194
+ GET request that returns a raw HTML string with `Content-Type: text/html`.
195
+
196
+ ```ts
197
+ const page = procedure.html(async ({ ctx }) => {
198
+ return `<!DOCTYPE html><html><body>Hello</body></html>`;
199
+ });
200
+ ```
201
+
202
+ ### Subscription
203
+
204
+ WebSocket-based pub/sub. Clients subscribe to a topic and receive messages when your server publishes.
205
+
206
+ ```ts
207
+ const messages = procedure
208
+ .input(z.object({ roomId: z.string(), text: z.string() }))
209
+ .subscription(async ({ ctx, input }) => {
210
+ await saveMessage(input);
211
+ return input; // returned value is broadcast to all subscribers
212
+ });
213
+ ```
214
+
215
+ Publish from anywhere on the server:
216
+
217
+ ```ts
218
+ router.publish(messages, { roomId: "general", text: "hello" }, { roomId: "general" });
219
+ ```
220
+
221
+ Parameters in the route path (e.g. `":roomId"`) scope the subscription topic — subscribers only receive messages matching their params.
222
+
223
+ ---
224
+
225
+ ## Middlewares
226
+
227
+ ### createMiddleware
228
+
229
+ Define reusable, typed middlewares outside of the procedure chain. Return an object to extend the context type — the returned fields are merged into `ctx` for the rest of the chain.
230
+
231
+ ```ts
232
+ import { createMiddleware, BRPCError } from "@mateosuarezdev/brpc";
233
+ import { context } from "./context";
234
+
235
+ // Pass context for automatic type inference
236
+ const authMiddleware = createMiddleware(context, async (ctx) => {
237
+ const session = await getSession(ctx.req);
238
+ if (!session) throw new BRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });
239
+ return { session };
240
+ });
241
+
242
+ // Or use an explicit generic when no context reference is available
243
+ const langMiddleware = createMiddleware<typeof context>(async (ctx) => {
244
+ const lang = ctx.req.headers.get("accept-language") ?? "en";
245
+ return { lang };
246
+ });
247
+ ```
248
+
249
+ Use middlewares on individual procedures:
250
+
251
+ ```ts
252
+ const protectedProcedure = procedure.use(authMiddleware);
253
+
254
+ const getProfile = protectedProcedure.query(async ({ ctx }) => {
255
+ ctx.session.userId; // fully typed
256
+ });
257
+ ```
258
+
259
+ Chain multiple middlewares — each one's return type accumulates into `ctx`:
260
+
261
+ ```ts
262
+ const localizedProtectedProcedure = procedure
263
+ .use(authMiddleware) // ctx gains { session }
264
+ .use(langMiddleware); // ctx gains { lang }
265
+
266
+ const getContent = localizedProtectedProcedure.query(async ({ ctx }) => {
267
+ ctx.session; // typed
268
+ ctx.lang; // typed
269
+ });
270
+ ```
271
+
272
+ Middlewares that only guard (no context extension) just don't return anything:
273
+
274
+ ```ts
275
+ const adminMiddleware = createMiddleware(context, async (ctx) => {
276
+ if (ctx.session.role !== "admin") {
277
+ throw new BRPCError({ code: "FORBIDDEN", message: "Admins only" });
278
+ }
279
+ // no return — ctx type unchanged
280
+ });
281
+ ```
282
+
283
+ ### Built-in Middlewares
284
+
285
+ #### Rate Limiter
286
+
287
+ ```ts
288
+ import { createRateLimiter } from "@mateosuarezdev/brpc";
289
+
290
+ const rateLimiter = createRateLimiter({
291
+ windowMs: 60_000, // 1 minute window
292
+ maxRequests: 100, // max 100 requests per window
293
+ maxEntries: 10_000, // max IPs to track
294
+ message: "Too many requests",
295
+ statusCode: 429,
296
+ headerPrefix: "X-RateLimit",
297
+ });
298
+ ```
299
+
300
+ Sets `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers automatically.
301
+
302
+ #### Path Blocker
303
+
304
+ Blocks requests matching any of the given regex patterns with a 404.
305
+
306
+ ```ts
307
+ import { createPathBlocker } from "@mateosuarezdev/brpc";
308
+
309
+ const blocker = createPathBlocker({
310
+ paths: ["/wp-admin", "\\.env", "/\\.git"],
311
+ });
312
+ ```
313
+
314
+ #### Profanity Filter
315
+
316
+ Scans request body, query params, and/or route params for profanity.
317
+
318
+ ```ts
319
+ import { createProfanityMiddleware } from "@mateosuarezdev/brpc";
320
+
321
+ const profanityFilter = createProfanityMiddleware({
322
+ languages: ["en", "es"],
323
+ checkBody: true,
324
+ checkQuery: true,
325
+ checkParams: false,
326
+ message: "Inappropriate content detected",
327
+ customLanguages: {
328
+ custom: { badWords: ["forbidden"], badPhrases: ["bad phrase"] },
329
+ },
330
+ });
331
+ ```
332
+
333
+ ---
334
+
335
+ ## Router
336
+
337
+ ```ts
338
+ import { createRouter } from "@mateosuarezdev/brpc";
339
+
340
+ const router = createRouter({
341
+ context,
342
+ routes,
343
+
344
+ // Optional
345
+ globalMiddlewares: [blocker, rateLimiter],
346
+ prefix: "/api",
347
+ debug: false,
348
+ autoFileCacheControl: true,
349
+
350
+ websocket: {
351
+ onOpen: (ws, ctx) => console.log("connected"),
352
+ onClose: (ws, code, reason, ctx) => console.log("disconnected"),
353
+ },
354
+
355
+ integrations: {
356
+ betterAuth: {
357
+ handler: auth.handler,
358
+ },
359
+ rawRoutes: {
360
+ "/health": async (req) => new Response("ok"),
361
+ "/webhook": {
362
+ POST: async (req) => handleWebhook(req),
363
+ },
364
+ },
365
+ },
366
+
367
+ onError: (error, { req, route }) => {
368
+ console.error(`Error on ${route}:`, error);
369
+ },
370
+ });
371
+
372
+ router.listen(3000);
373
+ ```
374
+
375
+ ### Global Middlewares and Context
376
+
377
+ `globalMiddlewares` run before every request, in order. They receive and can mutate `ctx` at runtime exactly like procedure middlewares — returning an object merges it into `ctx`.
378
+
379
+ However, because `C` is fixed at router creation time, TypeScript cannot widen the context type from a global middleware's return value. If a global middleware needs to add typed fields to `ctx`, declare them in `createContext` so they are part of `C` from the start:
380
+
381
+ ```ts
382
+ // context.ts — declare fields that global middlewares will populate
383
+ export const context = createContext(async (req) => ({
384
+ db: myDb,
385
+ session: null as Session | null, // global auth middleware fills this
386
+ }));
387
+
388
+ // router.ts
389
+ createRouter({
390
+ context,
391
+ routes,
392
+ globalMiddlewares: [
393
+ async (ctx) => {
394
+ ctx.session = await getSession(ctx.req) ?? null; // typed, works fine
395
+ },
396
+ ],
397
+ });
398
+ ```
399
+
400
+ At runtime, returning an object from a global middleware also works and merges into `ctx`, but any extra fields added this way won't be reflected in the TypeScript type — use direct assignment on `ctx` instead for global middlewares.
401
+
402
+ ### RouterConfig Options
403
+
404
+ | Option | Type | Description |
405
+ |---|---|---|
406
+ | `context` | `fn` | Context creator — receives `Request`, returns your custom data |
407
+ | `routes` | `Routes` | Nested route object |
408
+ | `globalMiddlewares` | `Middleware[]` | Run before every request |
409
+ | `prefix` | `string` | Path prefix for all routes |
410
+ | `debug` | `boolean` | Enable route debug logging |
411
+ | `autoFileCacheControl` | `boolean` | Auto-set cache headers for file procedures |
412
+ | `websocket.onOpen` | `fn` | Called when a WebSocket connection opens |
413
+ | `websocket.onClose` | `fn` | Called when a WebSocket connection closes |
414
+ | `integrations.betterAuth` | `{ handler }` | Delegate `better-auth` routes to its handler |
415
+ | `integrations.rawRoutes` | `Record<path, fn>` | Escape hatch for raw Bun routes |
416
+ | `onError` | `fn` | Global error handler |
417
+
418
+ ### Dynamic Route Parameters
419
+
420
+ Prefix a segment with `:` to capture it as a param:
421
+
422
+ ```ts
423
+ const routes = {
424
+ users: {
425
+ ":id": procedure
426
+ .query(async ({ ctx }) => {
427
+ ctx.params.id; // the captured value
428
+ return getUser(ctx.params.id);
429
+ }),
430
+ },
431
+ };
432
+ ```
433
+
434
+ ### Per-Procedure Timeout
435
+
436
+ Override the default 30s request timeout on any procedure:
437
+
438
+ ```ts
439
+ const slowQuery = procedure
440
+ .input(z.object({ q: z.string() }))
441
+ .timeout(120_000) // 2 minutes
442
+ .query(async ({ ctx, input }) => heavyComputation(input.q));
443
+ ```
444
+
445
+ ### Testing Without a Server
446
+
447
+ ```ts
448
+ const response = await router.testRequest(
449
+ new Request("http://localhost/hello?input=%7B%22name%22%3A%22world%22%7D")
450
+ );
451
+ ```
452
+
453
+ ---
454
+
455
+ ## Client
456
+
457
+ Import from `@mateosuarezdev/brpc/client`:
458
+
459
+ ```ts
460
+ import { createBrpcClient } from "@mateosuarezdev/brpc/client";
461
+ import type { AppRoutes } from "./routes";
462
+
463
+ const client = createBrpcClient<AppRoutes>({
464
+ headers: async () => ({
465
+ Authorization: `Bearer ${getToken()}`,
466
+ }),
467
+ prefix: "/api",
468
+ debug: false,
469
+ });
470
+
471
+ // Query
472
+ const user = await client.routes.users.getById.query({ id: "123" });
473
+
474
+ // Mutation
475
+ const post = await client.routes.posts.create.mutation({ title: "Hello" });
476
+
477
+ // FormMutation
478
+ const result = await client.routes.media.upload.formMutation({ file: myFile });
479
+
480
+ // File/FileStream — returns a URL string
481
+ const url = await client.routes.avatar.file();
482
+
483
+ // HTML — returns raw HTML string
484
+ const html = await client.routes.page.html();
485
+
486
+ // Subscription
487
+ const { unsubscribe, publish } = client.routes.messages[":roomId"].subscription(
488
+ (message) => console.log("received:", message),
489
+ );
490
+ publish({ roomId: "general", text: "hello" });
491
+ unsubscribe();
492
+ ```
493
+
494
+ ### BrpcClientOptions
495
+
496
+ | Option | Type | Description |
497
+ |---|---|---|
498
+ | `headers` | `fn \| Headers` | Default headers for all requests |
499
+ | `fetch` | `typeof fetch` | Custom fetch implementation |
500
+ | `WebSocket` | `typeof WebSocket` | Custom WebSocket implementation |
501
+ | `prefix` | `string` | API path prefix |
502
+ | `apiPrefix` | `string` | Additional API prefix |
503
+ | `debug` | `boolean` | Enable client-side debug logging |
504
+
505
+ ### Cache Keys
506
+
507
+ Every procedure exposes helpers for use with query libraries like TanStack Query:
508
+
509
+ ```ts
510
+ client.routes.users.getById.getStringKey({ id: "123" }); // "users.getById:{'id':'123'}"
511
+ client.routes.users.getById.getArrayKey({ id: "123" }); // ["users", "getById", { id: "123" }]
512
+ client.routes.users.list.getNoInputsArrayKey(); // ["users", "list"]
513
+ ```
514
+
515
+ ### Utilities
516
+
517
+ ```ts
518
+ // Update WebSocket auth token (e.g. after token refresh)
519
+ await client.utils.updateWsAuth();
520
+
521
+ // Update headers at runtime
522
+ await client.utils.setHeader("Authorization", `Bearer ${newToken}`);
523
+ await client.utils.setHeaders({ "X-Custom": "value" });
524
+ ```
525
+
526
+ ### BrpcClientError
527
+
528
+ ```ts
529
+ import { BrpcClientError } from "@mateosuarezdev/brpc/client";
530
+
531
+ try {
532
+ await client.routes.auth.login.mutation({ email, password });
533
+ } catch (err) {
534
+ if (err instanceof BrpcClientError) {
535
+ err.status; // HTTP status code
536
+ err.code; // BRPCErrorCode string
537
+ err.clientCode; // custom client code if set
538
+ err.isUnauthorized(); // status === 401
539
+ err.isForbidden(); // status === 403
540
+ err.isNotFound(); // status === 404
541
+ err.isValidationError(); // status === 400
542
+ err.isClientError("INVALID_CREDS"); // clientCode === "INVALID_CREDS"
543
+ }
544
+ }
545
+ ```
546
+
547
+ ### React
548
+
549
+ Import from `@mateosuarezdev/brpc/client/react`:
550
+
551
+ ```ts
552
+ import { useSubscription } from "@mateosuarezdev/brpc/client/react";
553
+
554
+ function ChatRoom({ roomId }: { roomId: string }) {
555
+ const [messages, setMessages] = useState<Message[]>([]);
556
+
557
+ const { publish } = useSubscription(
558
+ (callback) => client.routes.messages[":roomId"].subscription(callback),
559
+ (message) => setMessages((prev) => [...prev, message]),
560
+ );
561
+
562
+ return (
563
+ <button onClick={() => publish({ roomId, text: "hello" })}>
564
+ Send
565
+ </button>
566
+ );
567
+ }
568
+ ```
569
+
570
+ ---
571
+
572
+ ## Schemas
573
+
574
+ ### createFileSchema
575
+
576
+ Validates `File` objects in `formMutation` inputs:
577
+
578
+ ```ts
579
+ import { createFileSchema } from "@mateosuarezdev/brpc";
580
+
581
+ z.object({
582
+ avatar: createFileSchema({
583
+ acceptedTypes: {
584
+ image: ["image/jpeg", "image/png"],
585
+ },
586
+ maxSize: 5, // MB
587
+ minSize: 0.1, // MB
588
+ messages: {
589
+ type: "Only JPEG and PNG are allowed",
590
+ maxSize: "File must be under 5MB",
591
+ },
592
+ }),
593
+
594
+ document: createFileSchema({
595
+ acceptedTypes: { document: "*" },
596
+ maxSize: 20,
597
+ }),
598
+
599
+ audio: createFileSchema({
600
+ acceptedTypes: { audio: "*" },
601
+ }),
602
+ })
603
+ ```
604
+
605
+ **Accepted type groups:** `image`, `video`, `audio`, `document`
606
+ Pass `"*"` to accept all types in a group, or an array of specific MIME type strings.
607
+
608
+ ---
609
+
610
+ ## Errors
611
+
612
+ ```ts
613
+ import { BRPCError } from "@mateosuarezdev/brpc";
614
+
615
+ // Constructor
616
+ throw new BRPCError({
617
+ code: "UNAUTHORIZED",
618
+ message: "Token expired",
619
+ clientCode: "TOKEN_EXPIRED", // readable code for the client
620
+ data: { expiredAt: new Date() },
621
+ });
622
+
623
+ // Static factory shortcuts
624
+ throw BRPCError.unauthorized("Token expired", "TOKEN_EXPIRED");
625
+ throw BRPCError.badRequest("Invalid input", "INVALID_EMAIL");
626
+ throw BRPCError.forbidden("Admins only");
627
+ throw BRPCError.notFound("User not found");
628
+ throw BRPCError.conflict("Email already in use", "EMAIL_TAKEN");
629
+ throw BRPCError.tooManyRequests();
630
+ throw BRPCError.internalServerError();
631
+ ```
632
+
633
+ ### Error Codes and HTTP Status Mapping
634
+
635
+ | Code | Status |
636
+ |---|---|
637
+ | `BAD_REQUEST` | 400 |
638
+ | `UNAUTHORIZED` | 401 |
639
+ | `FORBIDDEN` | 403 |
640
+ | `NOT_FOUND` | 404 |
641
+ | `CONFLICT` | 409 |
642
+ | `UNPROCESSABLE_CONTENT` | 422 |
643
+ | `TOO_MANY_REQUESTS` | 429 |
644
+ | `INTERNAL_SERVER_ERROR` | 500 |
645
+ | `SERVICE_UNAVAILABLE` | 503 |
646
+
647
+ Zod validation errors are automatically caught and returned as `BAD_REQUEST` (400) with field-level detail.
648
+
649
+ ---
650
+
651
+ ## Streaming Media
652
+
653
+ Use `streamMedia` inside a `fileStream` procedure to handle HTTP Range requests automatically:
654
+
655
+ ```ts
656
+ import { streamMedia } from "@mateosuarezdev/brpc";
657
+
658
+ const video = procedure.fileStream(async ({ ctx }) => {
659
+ const file = Bun.file(`./media/${ctx.params.filename}`);
660
+
661
+ return streamMedia(file, ctx.req, {
662
+ maxChunkSize: 2 * 1024 * 1024, // 2MB per chunk (default)
663
+ cacheMaxAge: 3600, // Cache-Control max-age in seconds
664
+ acceptedExtensions: [".mp4", ".webm", ".ogg"],
665
+ });
666
+ });
667
+ ```
668
+
669
+ Responds with `206 Partial Content` when the client sends a `Range` header, enabling native browser seek/scrub for `<video>` and `<audio>` elements.
670
+
671
+ ---
672
+
673
+ ## Type Inference Utilities
674
+
675
+ ```ts
676
+ import type {
677
+ InferProcedureInput,
678
+ InferProcedureOutput,
679
+ InferRouterOutput,
680
+ } from "@mateosuarezdev/brpc";
681
+
682
+ // Infer types from a procedure
683
+ type CreatePostInput = InferProcedureInput<typeof routes.posts.create>;
684
+ type CreatePostOutput = InferProcedureOutput<typeof routes.posts.create>;
685
+
686
+ // Infer the full output shape of a routes object
687
+ type AppOutput = InferRouterOutput<typeof routes>;
688
+ type UserOutput = AppOutput["users"]["getById"];
689
+ ```
690
+
691
+ ---
692
+
693
+ ## Environment Variables
694
+
695
+ | Variable | Default | Description |
696
+ |---|---|---|
697
+ | `MAX_WS_CONNECTIONS` | `1000` | Max simultaneous WebSocket connections |
698
+ | `WS_TIMEOUT` | `30000` | WebSocket idle timeout in ms |
699
+ | `MAX_REQUEST_SIZE` | `10485760` | Max request body size in bytes (10MB) |
700
+ | `REQUEST_TIMEOUT` | `30000` | Default procedure timeout in ms |
701
+ | `ROUTE_CACHE_SIZE` | `1000` | Route matcher cache size |
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/dist/context.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import type { BaseContext } from "./types";
2
- export declare function createContext<C extends BaseContext>(contextCreator: (req: Request) => Promise<C>): (req: Request) => Promise<C>;
2
+ export declare function createContext<TCustom extends Record<string, any>>(contextCreator: (req: Request) => Promise<TCustom>): (req: Request) => Promise<BaseContext & TCustom>;
3
+ export declare function createContext(contextCreator: (req: Request) => Promise<void>): (req: Request) => Promise<BaseContext>;