@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 +701 -0
- package/dist/auth/types.d.ts +1 -0
- package/dist/auth/utils.d.ts +1 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/types.d.ts +1 -0
- package/dist/context.d.ts +2 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +9 -8
- package/dist/middlewares/cors/index.d.ts +1 -0
- package/dist/middlewares/createMiddleware.d.ts +25 -0
- package/dist/middlewares/index.d.ts +1 -0
- package/dist/procedure.d.ts +4 -3
- package/dist/router/RouteMatcher.d.ts +3 -0
- package/dist/types.d.ts +2 -2
- package/package.json +103 -103
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<
|
|
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>;
|