blaizejs 0.9.2 → 0.10.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.
- package/README.md +302 -1031
- package/dist/{chunk-3ICDFF57.js → chunk-C2PTO7BZ.js} +3 -3
- package/dist/{chunk-GIZW5W7C.js → chunk-FL7RIGDR.js} +3 -3
- package/dist/{chunk-ULIQB554.js → chunk-JEG7DCHT.js} +3 -3
- package/dist/{chunk-I5FNWSJS.js → chunk-KK6W6LDB.js} +3 -3
- package/dist/chunk-KK6W6LDB.js.map +1 -0
- package/dist/{chunk-XIPQPFN5.js → chunk-KTMO67JQ.js} +3 -3
- package/dist/index.cjs +15 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +16 -17
- package/dist/index.js.map +1 -1
- package/dist/{internal-server-error-DVVTTWHD.js → internal-server-error-RIRTDP3D.js} +3 -3
- package/dist/{payload-too-large-error-QGRSATV5.js → payload-too-large-error-6HOOZBD6.js} +3 -3
- package/dist/{unsupported-media-type-error-VAT4HTL4.js → unsupported-media-type-error-HSTEA3TX.js} +3 -3
- package/dist/{validation-error-N57OM7AM.js → validation-error-V74LVJBA.js} +3 -3
- package/package.json +4 -4
- package/dist/chunk-I5FNWSJS.js.map +0 -1
- /package/dist/{chunk-3ICDFF57.js.map → chunk-C2PTO7BZ.js.map} +0 -0
- /package/dist/{chunk-GIZW5W7C.js.map → chunk-FL7RIGDR.js.map} +0 -0
- /package/dist/{chunk-ULIQB554.js.map → chunk-JEG7DCHT.js.map} +0 -0
- /package/dist/{chunk-XIPQPFN5.js.map → chunk-KTMO67JQ.js.map} +0 -0
- /package/dist/{internal-server-error-DVVTTWHD.js.map → internal-server-error-RIRTDP3D.js.map} +0 -0
- /package/dist/{payload-too-large-error-QGRSATV5.js.map → payload-too-large-error-6HOOZBD6.js.map} +0 -0
- /package/dist/{unsupported-media-type-error-VAT4HTL4.js.map → unsupported-media-type-error-HSTEA3TX.js.map} +0 -0
- /package/dist/{validation-error-N57OM7AM.js.map → validation-error-V74LVJBA.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,1217 +1,488 @@
|
|
|
1
|
-
# 🔥 BlaizeJS
|
|
1
|
+
# 🔥 BlaizeJS
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Call server functions like local functions — fully typed
|
|
4
4
|
|
|
5
5
|
[](https://badge.fury.io/js/blaizejs)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
9
|
[](https://github.com/jleajones/blaize/actions)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
BlaizeJS is a TypeScript-first backend framework that brings end-to-end type safety to Node.js APIs. Define your routes once, and get full autocomplete and type checking on both server and client — no code generation, no manual type syncing, no runtime overhead.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
### Recommended: Create a New Project
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
# Using pnpm (recommended)
|
|
21
|
-
pnpm dlx create-blaize-app my-app
|
|
22
|
-
|
|
23
|
-
# Using npm
|
|
24
|
-
npx create-blaize-app my-app
|
|
25
|
-
|
|
26
|
-
# Using yarn
|
|
27
|
-
yarn dlx create-blaize-app my-app
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
This sets up a fully configured project with TypeScript, file-based routing, and example routes.
|
|
31
|
-
|
|
32
|
-
### Manual Installation
|
|
33
|
-
|
|
34
|
-
Add BlaizeJS to an existing project:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
# Using pnpm
|
|
38
|
-
pnpm add blaizejs zod
|
|
39
|
-
|
|
40
|
-
# Using npm
|
|
41
|
-
npm install blaizejs zod
|
|
42
|
-
|
|
43
|
-
# Using yarn
|
|
44
|
-
yarn add blaizejs zod
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
## 🚀 Quick Start
|
|
15
|
+
## ✨ The Magic
|
|
50
16
|
|
|
51
17
|
```typescript
|
|
52
|
-
// src/app.ts
|
|
18
|
+
// src/app.ts — Create your server and typed route factory
|
|
53
19
|
import { Blaize, type InferContext } from 'blaizejs';
|
|
54
|
-
import { fileURLToPath } from 'node:url';
|
|
55
|
-
import path from 'node:path';
|
|
56
|
-
|
|
57
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
58
|
-
const __dirname = path.dirname(__filename);
|
|
59
20
|
|
|
60
21
|
const app = Blaize.createServer({
|
|
61
22
|
port: 3000,
|
|
62
|
-
routesDir:
|
|
23
|
+
routesDir: './src/routes',
|
|
63
24
|
});
|
|
64
25
|
|
|
65
|
-
// Create a typed route factory
|
|
66
|
-
type AppContext = InferContext
|
|
26
|
+
// Create a typed route factory — shares types across all routes
|
|
27
|
+
type AppContext = InferContext<typeof app>;
|
|
67
28
|
export const route = Blaize.Router.createRouteFactory<
|
|
68
29
|
AppContext['state'],
|
|
69
30
|
AppContext['services']
|
|
70
31
|
>();
|
|
71
32
|
|
|
72
33
|
await app.listen();
|
|
73
|
-
console.log('🔥 Server running at https://localhost:3000');
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
// src/routes/hello.ts
|
|
78
|
-
import { route } from '../app';
|
|
79
|
-
import { z } from 'zod';
|
|
80
|
-
|
|
81
|
-
// Named export — the name becomes the client method name
|
|
82
|
-
export const getHello = route.get({
|
|
83
|
-
schema: {
|
|
84
|
-
response: z.object({ message: z.string() }),
|
|
85
|
-
},
|
|
86
|
-
handler: async () => ({ message: 'Hello, BlaizeJS!' }),
|
|
87
|
-
});
|
|
88
34
|
```
|
|
89
35
|
|
|
90
36
|
```typescript
|
|
91
|
-
// src/
|
|
92
|
-
import { getHello } from './routes/hello';
|
|
93
|
-
|
|
94
|
-
export const routes = { getHello } as const;
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## 📋 Table of Contents
|
|
100
|
-
|
|
101
|
-
- [Server](#-server)
|
|
102
|
-
- [Route Creators](#-route-creators)
|
|
103
|
-
- [Middleware](#-middleware)
|
|
104
|
-
- [Plugins](#-plugins)
|
|
105
|
-
- [Error Classes](#-error-classes)
|
|
106
|
-
- [Logging](#-logging)
|
|
107
|
-
- [Utilities](#-utilities)
|
|
108
|
-
- [Context Reference](#-context-reference)
|
|
109
|
-
- [Testing](#-testing)
|
|
110
|
-
- [Roadmap](#-roadmap)
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## 🖥️ Server
|
|
115
|
-
|
|
116
|
-
### createServer
|
|
117
|
-
|
|
118
|
-
Creates and configures a BlaizeJS server instance.
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
import { createServer } from 'blaizejs';
|
|
122
|
-
|
|
123
|
-
const server = createServer(options?: ServerOptions);
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
#### Options
|
|
127
|
-
|
|
128
|
-
| Option | Type | Default | Description |
|
|
129
|
-
|--------|------|---------|-------------|
|
|
130
|
-
| `port` | `number` | `3000` | Port to listen on |
|
|
131
|
-
| `host` | `string` | `'localhost'` | Host to bind to |
|
|
132
|
-
| `routesDir` | `string` | — | Directory for file-based route discovery |
|
|
133
|
-
| `middleware` | `Middleware[]` | `[]` | Global middleware (runs for all routes) |
|
|
134
|
-
| `plugins` | `Plugin[]` | `[]` | Plugins to register |
|
|
135
|
-
| `http2` | `boolean` | `true` | Enable HTTP/2 (with HTTP/1.1 fallback) |
|
|
136
|
-
| `bodyLimits` | `object` | See below | Request body size limits |
|
|
137
|
-
|
|
138
|
-
**Body Limits:**
|
|
139
|
-
|
|
140
|
-
| Property | Type | Default | Description |
|
|
141
|
-
|----------|------|---------|-------------|
|
|
142
|
-
| `bodyLimits.json` | `number` | `1048576` (1MB) | Max JSON body size in bytes |
|
|
143
|
-
| `bodyLimits.form` | `number` | `1048576` (1MB) | Max form body size in bytes |
|
|
144
|
-
| `bodyLimits.text` | `number` | `1048576` (1MB) | Max text body size in bytes |
|
|
145
|
-
|
|
146
|
-
#### Server Instance
|
|
147
|
-
|
|
148
|
-
The returned server instance provides:
|
|
149
|
-
|
|
150
|
-
| Method/Property | Type | Description |
|
|
151
|
-
|-----------------|------|-------------|
|
|
152
|
-
| `listen(port?, host?)` | `Promise<Server>` | Start the server |
|
|
153
|
-
| `close(options?)` | `Promise<void>` | Stop the server gracefully |
|
|
154
|
-
| `use(middleware)` | `Server` | Add middleware (chainable) |
|
|
155
|
-
| `register(plugin)` | `Promise<Server>` | Register a plugin |
|
|
156
|
-
| `port` | `number` (readonly) | Current port |
|
|
157
|
-
| `host` | `string` (readonly) | Current host |
|
|
158
|
-
| `middleware` | `Middleware[]` (readonly) | Registered middleware |
|
|
159
|
-
|
|
160
|
-
#### Examples
|
|
161
|
-
|
|
162
|
-
**Basic Setup:**
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
import { createServer } from 'blaizejs';
|
|
166
|
-
|
|
167
|
-
const server = createServer({
|
|
168
|
-
port: 3000,
|
|
169
|
-
routesDir: './src/routes',
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
await server.listen();
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
**With Middleware:**
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
import { createServer, createMiddleware } from 'blaizejs';
|
|
179
|
-
|
|
180
|
-
const logger = createMiddleware({
|
|
181
|
-
name: 'logger',
|
|
182
|
-
handler: async (ctx, next) => {
|
|
183
|
-
console.log(`→ ${ctx.request.method} ${ctx.request.path}`);
|
|
184
|
-
await next();
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const server = createServer({
|
|
189
|
-
middleware: [logger],
|
|
190
|
-
routesDir: './src/routes',
|
|
191
|
-
});
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
**With Plugins:**
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
import { createServer } from 'blaizejs';
|
|
198
|
-
import { createCachePlugin } from '@blaizejs/plugin-cache';
|
|
199
|
-
import { createMetricsPlugin } from '@blaizejs/plugin-metrics';
|
|
200
|
-
|
|
201
|
-
const server = createServer({
|
|
202
|
-
plugins: [
|
|
203
|
-
createCachePlugin({ defaultTtl: 3600 }),
|
|
204
|
-
createMetricsPlugin({ enabled: true }),
|
|
205
|
-
],
|
|
206
|
-
routesDir: './src/routes',
|
|
207
|
-
});
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
**Custom Body Limits:**
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
const server = createServer({
|
|
214
|
-
bodyLimits: {
|
|
215
|
-
json: 10 * 1024 * 1024, // 10MB for JSON
|
|
216
|
-
form: 50 * 1024 * 1024, // 50MB for forms (file uploads)
|
|
217
|
-
text: 1 * 1024 * 1024, // 1MB for text
|
|
218
|
-
},
|
|
219
|
-
routesDir: './src/routes',
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## 📂 Route Creators
|
|
226
|
-
|
|
227
|
-
BlaizeJS provides two approaches to creating routes:
|
|
228
|
-
|
|
229
|
-
1. **Route Factory (Recommended)** — Create a typed router that shares context types across all routes
|
|
230
|
-
2. **Individual Route Creators** — Lower-level functions for specific use cases
|
|
231
|
-
|
|
232
|
-
### createRouteFactory (Recommended)
|
|
233
|
-
|
|
234
|
-
The route factory pattern provides automatic type inference from your server's middleware and plugins.
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
// src/app.ts
|
|
238
|
-
import { Blaize, type InferContext } from 'blaizejs';
|
|
239
|
-
|
|
240
|
-
// 1. Create your server with middleware and plugins
|
|
241
|
-
const app = Blaize.createServer({
|
|
242
|
-
port: 3000,
|
|
243
|
-
routesDir: './src/routes',
|
|
244
|
-
middleware: [authMiddleware],
|
|
245
|
-
plugins: [databasePlugin()],
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// 2. Infer context types from the server
|
|
249
|
-
type AppContext = InferContext;
|
|
250
|
-
|
|
251
|
-
// 3. Create a typed route factory
|
|
252
|
-
export const route = Blaize.Router.createRouteFactory<
|
|
253
|
-
AppContext['state'], // Types from middleware (e.g., { user: User })
|
|
254
|
-
AppContext['services'] // Types from plugins (e.g., { db: Database })
|
|
255
|
-
>();
|
|
256
|
-
|
|
257
|
-
await app.listen();
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
The route factory provides methods for all HTTP verbs:
|
|
261
|
-
|
|
262
|
-
| Method | HTTP Verb | Has Body |
|
|
263
|
-
|--------|-----------|----------|
|
|
264
|
-
| `route.get()` | GET | No |
|
|
265
|
-
| `route.post()` | POST | Yes |
|
|
266
|
-
| `route.put()` | PUT | Yes |
|
|
267
|
-
| `route.patch()` | PATCH | Yes |
|
|
268
|
-
| `route.delete()` | DELETE | No |
|
|
269
|
-
| `route.head()` | HEAD | No |
|
|
270
|
-
| `route.options()` | OPTIONS | No |
|
|
271
|
-
| `route.sse()` | GET (SSE) | No |
|
|
272
|
-
|
|
273
|
-
#### Using the Route Factory
|
|
274
|
-
|
|
275
|
-
```typescript
|
|
276
|
-
// src/routes/users/index.ts
|
|
277
|
-
import { route } from '../../app';
|
|
278
|
-
import { z } from 'zod';
|
|
279
|
-
|
|
280
|
-
// Named exports — these names become the client method names
|
|
281
|
-
export const listUsers = route.get({
|
|
282
|
-
schema: {
|
|
283
|
-
query: z.object({ limit: z.coerce.number().default(10) }),
|
|
284
|
-
response: z.array(userSchema),
|
|
285
|
-
},
|
|
286
|
-
handler: async (ctx) => {
|
|
287
|
-
// ctx.state.user is typed from authMiddleware!
|
|
288
|
-
// ctx.services.db is typed from databasePlugin!
|
|
289
|
-
return await ctx.services.db.users.findMany({
|
|
290
|
-
take: ctx.request.query.limit,
|
|
291
|
-
});
|
|
292
|
-
},
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
export const createUser = route.post({
|
|
296
|
-
schema: {
|
|
297
|
-
body: z.object({
|
|
298
|
-
name: z.string().min(1),
|
|
299
|
-
email: z.string().email(),
|
|
300
|
-
}),
|
|
301
|
-
response: userSchema,
|
|
302
|
-
},
|
|
303
|
-
handler: async (ctx) => {
|
|
304
|
-
return await ctx.services.db.users.create(ctx.request.body);
|
|
305
|
-
},
|
|
306
|
-
});
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
// src/routes/users/[userId].ts
|
|
37
|
+
// src/routes/users/[userId].ts — Routes get full type inference
|
|
311
38
|
import { route } from '../../app';
|
|
312
39
|
import { z } from 'zod';
|
|
313
|
-
import { NotFoundError, ForbiddenError } from 'blaizejs';
|
|
314
40
|
|
|
315
41
|
export const getUser = route.get({
|
|
316
42
|
schema: {
|
|
317
43
|
params: z.object({ userId: z.string().uuid() }),
|
|
318
|
-
response:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return user;
|
|
324
|
-
},
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
export const updateUser = route.put({
|
|
328
|
-
schema: {
|
|
329
|
-
params: z.object({ userId: z.string() }),
|
|
330
|
-
body: updateUserSchema,
|
|
331
|
-
response: userSchema,
|
|
332
|
-
},
|
|
333
|
-
handler: async (ctx, params) => {
|
|
334
|
-
// Check authorization using typed state
|
|
335
|
-
if (ctx.state.user?.id !== params.userId && ctx.state.user?.role !== 'admin') {
|
|
336
|
-
throw new ForbiddenError('Cannot update other users');
|
|
337
|
-
}
|
|
338
|
-
return await ctx.services.db.users.update(params.userId, ctx.request.body);
|
|
339
|
-
},
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
export const deleteUser = route.delete({
|
|
343
|
-
schema: {
|
|
344
|
-
params: z.object({ userId: z.string() }),
|
|
44
|
+
response: z.object({
|
|
45
|
+
id: z.string(),
|
|
46
|
+
name: z.string(),
|
|
47
|
+
email: z.string().email(),
|
|
48
|
+
}),
|
|
345
49
|
},
|
|
346
|
-
handler: async (ctx, params) => {
|
|
347
|
-
|
|
348
|
-
|
|
50
|
+
handler: async ({ ctx, params }) => {
|
|
51
|
+
// ctx.state and ctx.services are fully typed from middleware/plugins!
|
|
52
|
+
return await db.users.findById(params.userId);
|
|
349
53
|
},
|
|
350
54
|
});
|
|
351
55
|
```
|
|
352
56
|
|
|
353
57
|
```typescript
|
|
354
|
-
// src/app-type.ts — Export
|
|
58
|
+
// src/app-type.ts — Export your route registry for the client
|
|
59
|
+
import { getUser } from './routes/users/[userId]';
|
|
355
60
|
import { listUsers, createUser } from './routes/users';
|
|
356
|
-
import { getUser, updateUser, deleteUser } from './routes/users/[userId]';
|
|
357
61
|
|
|
358
62
|
export const routes = {
|
|
63
|
+
getUser,
|
|
359
64
|
listUsers,
|
|
360
65
|
createUser,
|
|
361
|
-
getUser,
|
|
362
|
-
updateUser,
|
|
363
|
-
deleteUser,
|
|
364
66
|
} as const;
|
|
365
67
|
```
|
|
366
68
|
|
|
367
|
-
#### SSE Routes with the Factory
|
|
368
|
-
|
|
369
69
|
```typescript
|
|
370
|
-
//
|
|
371
|
-
import {
|
|
372
|
-
import {
|
|
373
|
-
|
|
374
|
-
export const getJobStream = route.sse({
|
|
375
|
-
schema: {
|
|
376
|
-
params: z.object({ jobId: z.string().uuid() }),
|
|
377
|
-
events: {
|
|
378
|
-
progress: z.object({
|
|
379
|
-
percent: z.number().min(0).max(100),
|
|
380
|
-
message: z.string().optional(),
|
|
381
|
-
}),
|
|
382
|
-
complete: z.object({ result: z.unknown() }),
|
|
383
|
-
error: z.object({ code: z.string(), message: z.string() }),
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
handler: async (stream, ctx, params, logger) => {
|
|
387
|
-
const unsubscribe = ctx.services.queue.subscribe(params.jobId, {
|
|
388
|
-
onProgress: (percent, message) => stream.send('progress', { percent, message }),
|
|
389
|
-
onComplete: (result) => stream.send('complete', { result }),
|
|
390
|
-
onError: (code, message) => stream.send('error', { code, message }),
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
stream.onClose(() => {
|
|
394
|
-
unsubscribe();
|
|
395
|
-
logger.info('Client disconnected');
|
|
396
|
-
});
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
// Client usage:
|
|
401
|
-
// const stream = await client.$sse.getJobStream({ params: { jobId: '123' } });
|
|
402
|
-
// stream.on('progress', (data) => console.log(data.percent));
|
|
403
|
-
```
|
|
70
|
+
// client.ts — Full autocomplete, zero configuration
|
|
71
|
+
import { createClient } from '@blaizejs/client';
|
|
72
|
+
import { routes } from './server/app-type';
|
|
404
73
|
|
|
405
|
-
|
|
74
|
+
// Create client with URL and routes registry
|
|
75
|
+
const client = createClient('https://api.example.com', routes);
|
|
406
76
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
logger: BlaizeLogger // Request-scoped logger
|
|
413
|
-
) => Promise
|
|
77
|
+
// Methods use the EXPORT NAME — not the path!
|
|
78
|
+
const user = await client.$get.getUser({
|
|
79
|
+
params: { userId: '550e8400-e29b-41d4-a716-446655440000' },
|
|
80
|
+
});
|
|
81
|
+
// ^ user is typed as { id: string; name: string; email: string }
|
|
414
82
|
```
|
|
415
83
|
|
|
416
|
-
**
|
|
417
|
-
|
|
418
|
-
| Method | Description |
|
|
419
|
-
|--------|-------------|
|
|
420
|
-
| `send(event, data)` | Send a typed event to the client |
|
|
421
|
-
| `close()` | Close the SSE connection |
|
|
84
|
+
**Define once. Infer everywhere.** Your IDE knows every route, every parameter, every response shape — automatically.
|
|
422
85
|
|
|
423
86
|
---
|
|
424
87
|
|
|
425
|
-
|
|
88
|
+
## 🎯 Why BlaizeJS?
|
|
426
89
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
> **Note:** These are higher-order functions (they return functions). The route factory pattern above is recommended for most applications.
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
import {
|
|
433
|
-
createGetRoute,
|
|
434
|
-
createPostRoute,
|
|
435
|
-
createPutRoute,
|
|
436
|
-
createPatchRoute,
|
|
437
|
-
createDeleteRoute,
|
|
438
|
-
createHeadRoute,
|
|
439
|
-
createOptionsRoute,
|
|
440
|
-
createSSERoute,
|
|
441
|
-
} from 'blaizejs';
|
|
442
|
-
```
|
|
90
|
+
### 🔒 End-to-End Type Safety
|
|
443
91
|
|
|
444
|
-
|
|
92
|
+
Types flow from your Zod schemas through your handlers to your client calls. Change a response field and TypeScript catches it everywhere — no manual syncing required.
|
|
445
93
|
|
|
446
94
|
```typescript
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
export const GET = getRoute({
|
|
454
|
-
schema: {
|
|
455
|
-
params: z.object({ userId: z.string().uuid() }),
|
|
456
|
-
response: z.object({
|
|
457
|
-
id: z.string(),
|
|
458
|
-
name: z.string(),
|
|
459
|
-
}),
|
|
460
|
-
},
|
|
461
|
-
handler: async (ctx, params) => {
|
|
462
|
-
return await db.users.findById(params.userId);
|
|
463
|
-
},
|
|
95
|
+
// Define your schema once
|
|
96
|
+
const userSchema = z.object({
|
|
97
|
+
id: z.string(),
|
|
98
|
+
name: z.string(),
|
|
99
|
+
role: z.enum(['admin', 'user']), // Add a field here...
|
|
464
100
|
});
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
---
|
|
468
|
-
|
|
469
|
-
## 🔗 Middleware
|
|
470
|
-
|
|
471
|
-
### createMiddleware
|
|
472
|
-
|
|
473
|
-
Create middleware with typed state and service additions.
|
|
474
101
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const middleware = createMiddleware({
|
|
479
|
-
name?: string;
|
|
480
|
-
handler: (ctx: Context, next: NextFunction) => Promise;
|
|
481
|
-
skip?: (ctx: Context) => boolean;
|
|
482
|
-
debug?: boolean;
|
|
102
|
+
export const listUsers = route.get({
|
|
103
|
+
schema: { response: z.array(userSchema) },
|
|
104
|
+
handler: async () => getUsers(),
|
|
483
105
|
});
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
#### Options
|
|
487
|
-
|
|
488
|
-
| Option | Type | Description |
|
|
489
|
-
|--------|------|-------------|
|
|
490
|
-
| `name` | `string` | Middleware name (for debugging/logging) |
|
|
491
|
-
| `handler` | `function` | The middleware function |
|
|
492
|
-
| `skip` | `function` | Optional condition to skip this middleware |
|
|
493
|
-
| `debug` | `boolean` | Enable debug logging |
|
|
494
106
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
| `TState` | Properties added to `ctx.state` |
|
|
500
|
-
| `TServices` | Properties added to `ctx.services` |
|
|
107
|
+
// Export to routes registry, client automatically knows about `role`
|
|
108
|
+
const users = await client.$get.listUsers();
|
|
109
|
+
users[0].role; // ✅ Autocomplete: 'admin' | 'user'
|
|
110
|
+
```
|
|
501
111
|
|
|
502
|
-
|
|
112
|
+
### 📡 Real-Time Built In
|
|
503
113
|
|
|
504
|
-
|
|
114
|
+
Server-Sent Events with typed event schemas. Stream data to clients with the same type safety as your REST endpoints.
|
|
505
115
|
|
|
506
116
|
```typescript
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
console.log(`← ${ctx.response.statusCode} (${duration}ms)`);
|
|
117
|
+
// Server: Stream job progress
|
|
118
|
+
export const getJobStatus = route.sse({
|
|
119
|
+
schema: {
|
|
120
|
+
query: z.object({ jobId: z.string() }),
|
|
121
|
+
events: {
|
|
122
|
+
progress: z.object({ percent: z.number(), message: z.string() }),
|
|
123
|
+
complete: z.object({ result: z.string() }),
|
|
124
|
+
error: z.object({ code: z.string(), message: z.string() }),
|
|
125
|
+
},
|
|
517
126
|
},
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
```typescript
|
|
524
|
-
interface User {
|
|
525
|
-
id: string;
|
|
526
|
-
email: string;
|
|
527
|
-
role: 'admin' | 'user';
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
interface AuthService {
|
|
531
|
-
verify(token: string): Promise;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const authMiddleware = createMiddleware<
|
|
535
|
-
{ user: User },
|
|
536
|
-
{ auth: AuthService }
|
|
537
|
-
>({
|
|
538
|
-
name: 'auth',
|
|
539
|
-
handler: async (ctx, next) => {
|
|
540
|
-
const token = ctx.request.header('authorization')?.replace('Bearer ', '');
|
|
541
|
-
|
|
542
|
-
if (!token) {
|
|
543
|
-
throw new UnauthorizedError('Missing authentication token');
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
try {
|
|
547
|
-
ctx.state.user = await authService.verify(token);
|
|
548
|
-
ctx.services.auth = authService;
|
|
549
|
-
} catch (error) {
|
|
550
|
-
throw new UnauthorizedError('Invalid token');
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
await next();
|
|
127
|
+
handler: async ({ stream, ctx }) => {
|
|
128
|
+
stream.send('progress', { percent: 0, message: 'Starting...' });
|
|
129
|
+
// ... do work ...
|
|
130
|
+
stream.send('complete', { result: 'Done!' });
|
|
554
131
|
},
|
|
555
|
-
skip: (ctx) => ctx.request.path === '/health',
|
|
556
132
|
});
|
|
557
133
|
```
|
|
558
134
|
|
|
559
|
-
**Timing Middleware:**
|
|
560
|
-
|
|
561
135
|
```typescript
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
await next();
|
|
567
|
-
|
|
568
|
-
const duration = Date.now() - ctx.state.requestStart;
|
|
569
|
-
ctx.response.header('X-Response-Time', `${duration}ms`);
|
|
570
|
-
},
|
|
136
|
+
// Client: Typed event listeners (browser only)
|
|
137
|
+
const events = await client.$sse.getJobStatus({ query: { jobId: '123' } });
|
|
138
|
+
events.on('progress', data => {
|
|
139
|
+
console.log(`${data.percent}%: ${data.message}`);
|
|
571
140
|
});
|
|
572
141
|
```
|
|
573
142
|
|
|
574
|
-
###
|
|
575
|
-
|
|
576
|
-
Shorthand for middleware that only adds state.
|
|
577
|
-
|
|
578
|
-
```typescript
|
|
579
|
-
import { createStateMiddleware } from 'blaizejs';
|
|
580
|
-
|
|
581
|
-
const timingMiddleware = createStateMiddleware(
|
|
582
|
-
async (ctx, next) => {
|
|
583
|
-
ctx.state.startTime = Date.now();
|
|
584
|
-
await next();
|
|
585
|
-
}
|
|
586
|
-
);
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
### createServiceMiddleware
|
|
143
|
+
### ⚙️ Background Jobs with Progress Tracking
|
|
590
144
|
|
|
591
|
-
|
|
145
|
+
Built-in job queues with priority scheduling, retries, and real-time progress streaming via SSE.
|
|
592
146
|
|
|
593
147
|
```typescript
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
ctx.services.db = database;
|
|
599
|
-
await next();
|
|
600
|
-
}
|
|
601
|
-
);
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
### compose
|
|
148
|
+
// Define a job handler
|
|
149
|
+
const processVideo = async (ctx: JobContext<{ videoId: string }>) => {
|
|
150
|
+
ctx.progress(10, 'Downloading...');
|
|
151
|
+
const video = await download(ctx.data.videoId);
|
|
605
152
|
|
|
606
|
-
|
|
153
|
+
ctx.progress(50, 'Transcoding...');
|
|
154
|
+
const output = await transcode(video);
|
|
607
155
|
|
|
608
|
-
|
|
609
|
-
|
|
156
|
+
ctx.progress(90, 'Uploading...');
|
|
157
|
+
await upload(output);
|
|
610
158
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
authMiddleware,
|
|
614
|
-
timingMiddleware,
|
|
615
|
-
]);
|
|
159
|
+
return { url: output.url };
|
|
160
|
+
};
|
|
616
161
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
162
|
+
// Queue a job from any route
|
|
163
|
+
const jobId = await ctx.services.queue.add('media', 'process-video', {
|
|
164
|
+
videoId: '123',
|
|
620
165
|
});
|
|
621
166
|
```
|
|
622
167
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
## 🔌 Plugins
|
|
168
|
+
### 🛡️ Errors That Make Sense
|
|
626
169
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
Create a plugin with lifecycle hooks and service injection.
|
|
170
|
+
12 semantic error classes that automatically format to proper HTTP responses with correlation IDs for distributed tracing.
|
|
630
171
|
|
|
631
172
|
```typescript
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
173
|
+
// Throw semantic errors
|
|
174
|
+
if (!user) {
|
|
175
|
+
throw new NotFoundError('User not found', {
|
|
176
|
+
resourceType: 'user',
|
|
177
|
+
resourceId: userId,
|
|
178
|
+
suggestion: 'Verify the user ID exists',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Automatic HTTP response:
|
|
183
|
+
// {
|
|
184
|
+
// "type": "NOT_FOUND",
|
|
185
|
+
// "title": "User not found",
|
|
186
|
+
// "status": 404,
|
|
187
|
+
// "correlationId": "req_k3x2m1_9z8y7w6v",
|
|
188
|
+
// "timestamp": "2024-01-15T10:30:00.000Z",
|
|
189
|
+
// "details": {
|
|
190
|
+
// "resourceType": "user",
|
|
191
|
+
// "resourceId": "123",
|
|
192
|
+
// "suggestion": "Verify the user ID exists"
|
|
193
|
+
// }
|
|
194
|
+
// }
|
|
640
195
|
```
|
|
641
196
|
|
|
642
|
-
|
|
197
|
+
---
|
|
643
198
|
|
|
644
|
-
|
|
645
|
-
|-----------|------|-------------|
|
|
646
|
-
| `name` | `string` | Unique plugin identifier |
|
|
647
|
-
| `version` | `string` | Plugin version (SemVer) |
|
|
648
|
-
| `setup` | `function` | Setup function called during registration |
|
|
649
|
-
| `defaultOptions` | `object` | Default option values |
|
|
199
|
+
## 🚀 Quick Start
|
|
650
200
|
|
|
651
|
-
|
|
201
|
+
### Create a New Project
|
|
652
202
|
|
|
653
|
-
|
|
654
|
-
|------|------|----------|
|
|
655
|
-
| `register` | During `createServer()` | Add middleware, register routes |
|
|
656
|
-
| `initialize` | Before `server.listen()` | Connect to databases, warm caches |
|
|
657
|
-
| `onServerStart` | After server is listening | Start background workers |
|
|
658
|
-
| `onServerStop` | Before `server.close()` | Stop accepting work |
|
|
659
|
-
| `terminate` | During shutdown | Disconnect resources |
|
|
203
|
+
The fastest way to get started is with `create-blaize-app`:
|
|
660
204
|
|
|
661
|
-
|
|
205
|
+
```bash
|
|
206
|
+
# Using pnpm (recommended)
|
|
207
|
+
pnpm dlx create-blaize-app my-app
|
|
662
208
|
|
|
663
|
-
|
|
209
|
+
# Using npm
|
|
210
|
+
npx create-blaize-app my-app
|
|
664
211
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
'hello',
|
|
668
|
-
'1.0.0',
|
|
669
|
-
(server) => {
|
|
670
|
-
console.log('Hello plugin registered!');
|
|
671
|
-
|
|
672
|
-
return {
|
|
673
|
-
onServerStart: () => {
|
|
674
|
-
console.log('Server started!');
|
|
675
|
-
},
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
);
|
|
679
|
-
|
|
680
|
-
const server = createServer({
|
|
681
|
-
plugins: [helloPlugin()],
|
|
682
|
-
});
|
|
212
|
+
# Using yarn
|
|
213
|
+
yarn dlx create-blaize-app my-app
|
|
683
214
|
```
|
|
684
215
|
|
|
685
|
-
|
|
216
|
+
```bash
|
|
217
|
+
cd my-app
|
|
218
|
+
pnpm dev
|
|
219
|
+
# 🔥 Server running at https://localhost:7485
|
|
220
|
+
```
|
|
686
221
|
|
|
687
|
-
|
|
688
|
-
interface DatabaseOptions {
|
|
689
|
-
connectionString: string;
|
|
690
|
-
poolSize?: number;
|
|
691
|
-
}
|
|
222
|
+
### Verify It Works
|
|
692
223
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
(server, options) => {
|
|
697
|
-
let db: Database;
|
|
698
|
-
|
|
699
|
-
// Inject database into context
|
|
700
|
-
server.use(createMiddleware({
|
|
701
|
-
name: 'database-injection',
|
|
702
|
-
handler: async (ctx, next) => {
|
|
703
|
-
ctx.services.db = db;
|
|
704
|
-
await next();
|
|
705
|
-
},
|
|
706
|
-
}));
|
|
707
|
-
|
|
708
|
-
return {
|
|
709
|
-
initialize: async () => {
|
|
710
|
-
db = await Database.connect(options.connectionString, {
|
|
711
|
-
poolSize: options.poolSize,
|
|
712
|
-
});
|
|
713
|
-
console.log('Database connected');
|
|
714
|
-
},
|
|
715
|
-
terminate: async () => {
|
|
716
|
-
await db.disconnect();
|
|
717
|
-
console.log('Database disconnected');
|
|
718
|
-
},
|
|
719
|
-
};
|
|
720
|
-
},
|
|
721
|
-
{ poolSize: 10 } // Default options
|
|
722
|
-
);
|
|
723
|
-
|
|
724
|
-
// Usage
|
|
725
|
-
const server = createServer({
|
|
726
|
-
plugins: [
|
|
727
|
-
databasePlugin({ connectionString: 'postgres://localhost/mydb' }),
|
|
728
|
-
],
|
|
729
|
-
});
|
|
224
|
+
```bash
|
|
225
|
+
curl -k https://localhost:7485/health
|
|
226
|
+
# {"status":"ok","timestamp":1703001234567}
|
|
730
227
|
```
|
|
731
228
|
|
|
732
|
-
|
|
229
|
+
That's it! You have a fully configured BlaizeJS project with TypeScript, file-based routing, and example routes.
|
|
733
230
|
|
|
734
|
-
|
|
231
|
+
<details>
|
|
232
|
+
<summary><strong>📦 Manual Installation</strong></summary>
|
|
735
233
|
|
|
736
|
-
|
|
234
|
+
If you prefer to add BlaizeJS to an existing project:
|
|
737
235
|
|
|
738
|
-
|
|
236
|
+
```bash
|
|
237
|
+
# Using pnpm
|
|
238
|
+
pnpm add blaizejs zod
|
|
739
239
|
|
|
740
|
-
|
|
240
|
+
# Using npm
|
|
241
|
+
npm install blaizejs zod
|
|
741
242
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
"type": "ERROR_TYPE",
|
|
745
|
-
"title": "Error message",
|
|
746
|
-
"status": 400,
|
|
747
|
-
"correlationId": "req_k3x2m1_9z8y7w6v",
|
|
748
|
-
"timestamp": "2024-01-15T10:30:00.000Z",
|
|
749
|
-
"details": { }
|
|
750
|
-
}
|
|
243
|
+
# Using yarn
|
|
244
|
+
yarn add blaizejs zod
|
|
751
245
|
```
|
|
752
246
|
|
|
753
|
-
### Error Classes Reference
|
|
754
|
-
|
|
755
|
-
| Class | Status | Use Case |
|
|
756
|
-
|-------|--------|----------|
|
|
757
|
-
| `ValidationError` | 400 | Schema validation failures, invalid input |
|
|
758
|
-
| `UnauthorizedError` | 401 | Missing or invalid authentication |
|
|
759
|
-
| `ForbiddenError` | 403 | Authenticated but not authorized |
|
|
760
|
-
| `NotFoundError` | 404 | Resource doesn't exist |
|
|
761
|
-
| `ConflictError` | 409 | Resource state conflict (duplicate, version mismatch) |
|
|
762
|
-
| `RequestTimeoutError` | 408 | Request took too long |
|
|
763
|
-
| `PayloadTooLargeError` | 413 | Request body exceeds limit |
|
|
764
|
-
| `UnsupportedMediaTypeError` | 415 | Wrong content type |
|
|
765
|
-
| `UnprocessableEntityError` | 422 | Valid syntax but invalid semantics |
|
|
766
|
-
| `RateLimitError` | 429 | Too many requests |
|
|
767
|
-
| `InternalServerError` | 500 | Unexpected server error |
|
|
768
|
-
| `ServiceNotAvailableError` | 503 | Dependency unavailable |
|
|
769
|
-
|
|
770
|
-
### Usage
|
|
771
|
-
|
|
772
247
|
```typescript
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
ForbiddenError,
|
|
778
|
-
ConflictError,
|
|
779
|
-
RateLimitError,
|
|
780
|
-
} from 'blaizejs';
|
|
781
|
-
|
|
782
|
-
// Basic usage
|
|
783
|
-
throw new NotFoundError('User not found');
|
|
784
|
-
|
|
785
|
-
// With details
|
|
786
|
-
throw new NotFoundError('User not found', {
|
|
787
|
-
resourceType: 'user',
|
|
788
|
-
resourceId: userId,
|
|
789
|
-
suggestion: 'Verify the user ID exists',
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
// Validation error with field details
|
|
793
|
-
throw new ValidationError('Invalid input', {
|
|
794
|
-
fields: {
|
|
795
|
-
email: 'Must be a valid email address',
|
|
796
|
-
age: 'Must be at least 18',
|
|
797
|
-
},
|
|
798
|
-
});
|
|
248
|
+
// src/app.ts
|
|
249
|
+
import { Blaize, type InferContext } from 'blaizejs';
|
|
250
|
+
import { fileURLToPath } from 'node:url';
|
|
251
|
+
import path from 'node:path';
|
|
799
252
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
retryAfter: 60,
|
|
803
|
-
limit: 100,
|
|
804
|
-
remaining: 0,
|
|
805
|
-
});
|
|
253
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
254
|
+
const __dirname = path.dirname(__filename);
|
|
806
255
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
providedVersion: 3,
|
|
256
|
+
const app = Blaize.createServer({
|
|
257
|
+
port: 3000,
|
|
258
|
+
routesDir: path.resolve(__dirname, './routes'),
|
|
811
259
|
});
|
|
812
|
-
```
|
|
813
|
-
|
|
814
|
-
### Custom Correlation ID
|
|
815
|
-
|
|
816
|
-
```typescript
|
|
817
|
-
// Use custom correlation ID (for distributed tracing)
|
|
818
|
-
throw new NotFoundError('User not found', {}, 'custom-trace-id-123');
|
|
819
|
-
```
|
|
820
260
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
```typescript
|
|
828
|
-
import { logger } from 'blaizejs';
|
|
261
|
+
// Create typed route factory
|
|
262
|
+
type AppContext = InferContext<typeof app>;
|
|
263
|
+
export const route = Blaize.Router.createRouteFactory<
|
|
264
|
+
AppContext['state'],
|
|
265
|
+
AppContext['services']
|
|
266
|
+
>();
|
|
829
267
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
logger.error('Database error', { error: err.message });
|
|
833
|
-
logger.debug('Request received', { path: '/users' });
|
|
268
|
+
await app.listen();
|
|
269
|
+
console.log('🔥 Server running at https://localhost:3000');
|
|
834
270
|
```
|
|
835
271
|
|
|
836
|
-
### createLogger
|
|
837
|
-
|
|
838
|
-
Create a custom logger with specific transports.
|
|
839
|
-
|
|
840
272
|
```typescript
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
level: 'info',
|
|
845
|
-
transports: [
|
|
846
|
-
new ConsoleTransport({ colorize: true }),
|
|
847
|
-
new JSONTransport({ destination: './logs/app.log' }),
|
|
848
|
-
],
|
|
849
|
-
});
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
### Available Transports
|
|
853
|
-
|
|
854
|
-
| Transport | Description |
|
|
855
|
-
|-----------|-------------|
|
|
856
|
-
| `ConsoleTransport` | Logs to stdout with optional colors |
|
|
857
|
-
| `JSONTransport` | Logs as JSON to file or stream |
|
|
858
|
-
| `NullTransport` | Discards all logs (for testing) |
|
|
859
|
-
|
|
860
|
-
### Route Handler Logger
|
|
861
|
-
|
|
862
|
-
Route handlers receive a request-scoped logger as the third parameter:
|
|
273
|
+
// src/routes/health.ts
|
|
274
|
+
import { route } from '../app';
|
|
275
|
+
import { z } from 'zod';
|
|
863
276
|
|
|
864
|
-
|
|
865
|
-
export const getUser = route.get({
|
|
277
|
+
export const getHealth = route.get({
|
|
866
278
|
schema: {
|
|
867
|
-
|
|
868
|
-
},
|
|
869
|
-
handler: async (ctx, params, logger) => {
|
|
870
|
-
logger.info('Fetching user', { userId: params.userId });
|
|
871
|
-
// Log includes correlation ID automatically
|
|
872
|
-
|
|
873
|
-
const user = await db.users.findById(params.userId);
|
|
874
|
-
|
|
875
|
-
logger.debug('User found', { user });
|
|
876
|
-
return user;
|
|
279
|
+
response: z.object({ status: z.literal('ok'), timestamp: z.number() }),
|
|
877
280
|
},
|
|
281
|
+
handler: async () => ({
|
|
282
|
+
status: 'ok' as const,
|
|
283
|
+
timestamp: Date.now(),
|
|
284
|
+
}),
|
|
878
285
|
});
|
|
879
286
|
```
|
|
880
287
|
|
|
881
|
-
### configureGlobalLogger
|
|
882
|
-
|
|
883
|
-
Configure the global logger instance.
|
|
884
|
-
|
|
885
288
|
```typescript
|
|
886
|
-
|
|
289
|
+
// src/app-type.ts — Export routes registry for the client
|
|
290
|
+
import { getHealth } from './routes/health';
|
|
887
291
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
new JSONTransport({ pretty: process.env.NODE_ENV !== 'production' }),
|
|
892
|
-
],
|
|
893
|
-
});
|
|
292
|
+
export const routes = {
|
|
293
|
+
getHealth,
|
|
294
|
+
} as const;
|
|
894
295
|
```
|
|
895
296
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
## 🛠️ Utilities
|
|
899
|
-
|
|
900
|
-
### getCorrelationId
|
|
901
|
-
|
|
902
|
-
Get the current request's correlation ID (from AsyncLocalStorage).
|
|
903
|
-
|
|
904
|
-
```typescript
|
|
905
|
-
import { getCorrelationId } from 'blaizejs';
|
|
906
|
-
|
|
907
|
-
function someDeepFunction() {
|
|
908
|
-
const correlationId = getCorrelationId();
|
|
909
|
-
console.log(`Processing request ${correlationId}`);
|
|
910
|
-
}
|
|
911
|
-
```
|
|
297
|
+
</details>
|
|
912
298
|
|
|
913
|
-
###
|
|
299
|
+
### Add a Type-Safe Client
|
|
914
300
|
|
|
915
|
-
|
|
301
|
+
Connect to your API with full type inference:
|
|
916
302
|
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const server = createServer({
|
|
921
|
-
middleware: [
|
|
922
|
-
cors({
|
|
923
|
-
origin: 'https://example.com',
|
|
924
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
925
|
-
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
926
|
-
credentials: true,
|
|
927
|
-
maxAge: 86400,
|
|
928
|
-
}),
|
|
929
|
-
],
|
|
930
|
-
routesDir: './routes',
|
|
931
|
-
});
|
|
303
|
+
```bash
|
|
304
|
+
pnpm add @blaizejs/client
|
|
932
305
|
```
|
|
933
306
|
|
|
934
|
-
#### CORS Options
|
|
935
|
-
|
|
936
|
-
| Option | Type | Default | Description |
|
|
937
|
-
|--------|------|---------|-------------|
|
|
938
|
-
| `origin` | `string \| string[] \| function` | `'*'` | Allowed origins |
|
|
939
|
-
| `methods` | `string[]` | `['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']` | Allowed methods |
|
|
940
|
-
| `allowedHeaders` | `string[]` | — | Allowed request headers |
|
|
941
|
-
| `exposedHeaders` | `string[]` | — | Headers to expose to client |
|
|
942
|
-
| `credentials` | `boolean` | `false` | Allow credentials |
|
|
943
|
-
| `maxAge` | `number` | — | Preflight cache duration (seconds) |
|
|
944
|
-
|
|
945
|
-
#### Examples
|
|
946
|
-
|
|
947
|
-
**Multiple Origins:**
|
|
948
|
-
|
|
949
307
|
```typescript
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
```
|
|
308
|
+
// client.ts
|
|
309
|
+
import { createClient } from '@blaizejs/client';
|
|
310
|
+
import { routes } from './server/app-type';
|
|
954
311
|
|
|
955
|
-
|
|
312
|
+
// Create client with URL and routes registry
|
|
313
|
+
const client = createClient('https://localhost:3000', routes);
|
|
956
314
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
},
|
|
962
|
-
});
|
|
315
|
+
// Methods use the EXPORT NAME from your routes
|
|
316
|
+
const health = await client.$get.getHealth();
|
|
317
|
+
console.log(health.status); // ✅ Typed as 'ok'
|
|
318
|
+
console.log(health.timestamp); // ✅ Typed as number
|
|
963
319
|
```
|
|
964
320
|
|
|
965
321
|
---
|
|
966
322
|
|
|
967
|
-
## 📦
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
|
974
|
-
|
|
975
|
-
| `
|
|
976
|
-
| `
|
|
977
|
-
| `
|
|
978
|
-
| `
|
|
979
|
-
| `
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
|
984
|
-
|
|
985
|
-
| `
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
|
990
|
-
|-----------------|------|-------------|
|
|
991
|
-
| `sent` | `boolean` | Whether response was sent |
|
|
992
|
-
| `statusCode` | `number` | Current status code |
|
|
993
|
-
| `status(code)` | `ContextResponse` | Set status code (chainable) |
|
|
994
|
-
| `json(data)` | `ContextResponse` | Send JSON response |
|
|
995
|
-
| `html(content)` | `ContextResponse` | Send HTML response |
|
|
996
|
-
| `text(content)` | `ContextResponse` | Send text response |
|
|
997
|
-
| `redirect(url, code?)` | `ContextResponse` | Redirect response |
|
|
998
|
-
| `stream(readable)` | `ContextResponse` | Stream response |
|
|
999
|
-
| `header(name, value)` | `ContextResponse` | Set a header (chainable) |
|
|
1000
|
-
| `headers(headers)` | `ContextResponse` | Set multiple headers |
|
|
1001
|
-
|
|
1002
|
-
### ctx.state
|
|
1003
|
-
|
|
1004
|
-
Request-scoped data added by middleware:
|
|
323
|
+
## 📦 Ecosystem
|
|
324
|
+
|
|
325
|
+
| Package | Version | Description | Status |
|
|
326
|
+
|---------|---------|-------------|--------|
|
|
327
|
+
| [`blaizejs`](./packages/blaize-core) | 0.9.2 | Core framework | 🟡 Beta |
|
|
328
|
+
| [`@blaizejs/client`](./packages/blaize-client) | 0.5.1 | Type-safe RPC client | 🟡 Beta |
|
|
329
|
+
| [`@blaizejs/plugin-queue`](./plugins/queue) | 2.0.0 | Background job processing with Redis | 🟢 Stable |
|
|
330
|
+
| [`@blaizejs/plugin-cache`](./plugins/cache) | 2.0.0 | Caching with memory/Redis adapters | 🟢 Stable |
|
|
331
|
+
| [`@blaizejs/plugin-metrics`](./plugins/metrics) | 4.0.0 | Prometheus metrics & dashboard | 🟡 Beta |
|
|
332
|
+
| [`@blaizejs/middleware-security`](./middleware/security) | 4.0.0 | Security headers (CSP, HSTS) | 🟡 Beta |
|
|
333
|
+
| [`@blaizejs/testing-utils`](./packages/blaize-testing-utils) | 0.2.0 | Test helpers & mocks | 🟡 Beta |
|
|
334
|
+
| [`@blaizejs/adapter-redis`](./adapters/redis) | 2.0.0 | Redis adapter for queue & cache | 🟢 Stable |
|
|
335
|
+
| `create-blaize-app` | 0.1.21 | Project scaffolding CLI | 🟡 Beta |
|
|
336
|
+
|
|
337
|
+
### 🔮 Coming Soon
|
|
338
|
+
|
|
339
|
+
| Package | Description |
|
|
340
|
+
|---------|-------------|
|
|
341
|
+
| `@blaizejs/plugin-storage` | File storage abstraction (S3, local, etc.) |
|
|
342
|
+
| `@blaizejs/plugin-db` | Database integration with migrations |
|
|
343
|
+
| `@blaizejs/plugin-rate-limit` | Flexible rate limiting |
|
|
344
|
+
| `@blaizejs/middleware-compression` | Response compression |
|
|
345
|
+
| `@blaizejs/plugin-auth` | Authentication strategies |
|
|
1005
346
|
|
|
1006
|
-
|
|
1007
|
-
// Added by authMiddleware
|
|
1008
|
-
ctx.state.user // User object
|
|
347
|
+
---
|
|
1009
348
|
|
|
1010
|
-
|
|
1011
|
-
ctx.state.startTime // number
|
|
1012
|
-
```
|
|
349
|
+
## 📚 Documentation
|
|
1013
350
|
|
|
1014
|
-
###
|
|
351
|
+
### Getting Started
|
|
1015
352
|
|
|
1016
|
-
|
|
353
|
+
- [Quick Start](#-quick-start) — Zero to API in 5 minutes
|
|
354
|
+
- [30-Minute Tutorial](./docs/getting-started/tutorial.md) — Build a complete task API
|
|
355
|
+
- [Architecture Overview](./ARCHITECTURE.md) — How BlaizeJS works under the hood
|
|
1017
356
|
|
|
1018
|
-
|
|
1019
|
-
// Added by databasePlugin
|
|
1020
|
-
ctx.services.db // Database instance
|
|
357
|
+
### Core Guides
|
|
1021
358
|
|
|
1022
|
-
|
|
1023
|
-
|
|
359
|
+
- [Routing Guide](./docs/guides/routing.md) — File-based routing patterns
|
|
360
|
+
- [Middleware Guide](./docs/guides/middleware.md) — Request processing pipeline
|
|
361
|
+
- [Plugins Guide](./docs/guides/plugins.md) — Resource management & lifecycle
|
|
362
|
+
- [Error Handling Guide](./docs/guides/error-handling.md) — Semantic errors & debugging
|
|
363
|
+
- [Real-Time Guide](./docs/guides/real-time.md) — SSE and Event Bus
|
|
364
|
+
- [Client Guide](./docs/guides/client.md) — Type-safe RPC & SSE client
|
|
365
|
+
- [Testing Guide](./docs/guides/testing.md) — Test your BlaizeJS apps
|
|
1024
366
|
|
|
1025
|
-
|
|
1026
|
-
ctx.services.queue // QueueService
|
|
1027
|
-
```
|
|
367
|
+
### API Reference
|
|
1028
368
|
|
|
1029
|
-
|
|
369
|
+
- [`blaizejs`](./packages/blaize-core/README.md) — Core framework API
|
|
370
|
+
- [`@blaizejs/client`](./packages/blaize-client/README.md) — Client SDK API
|
|
371
|
+
- [`@blaizejs/plugin-queue`](./plugins/queue/README.md) — Queue plugin API
|
|
372
|
+
- [`@blaizejs/plugin-cache`](./plugins/cache/README.md) — Cache plugin API
|
|
373
|
+
- [`@blaizejs/testing-utils`](./packages/blaize-testing-utils/README.md) — Testing utilities API
|
|
1030
374
|
|
|
1031
375
|
---
|
|
1032
376
|
|
|
1033
|
-
##
|
|
377
|
+
## 🗺️ Roadmap
|
|
1034
378
|
|
|
1035
|
-
|
|
379
|
+
### ✅ v0.9.2 (Current)
|
|
1036
380
|
|
|
1037
|
-
|
|
381
|
+
- ✅ Event Bus for distributed coordination
|
|
382
|
+
- ✅ Redis adapter for queue and cache
|
|
383
|
+
- ✅ Type-safe file handling
|
|
384
|
+
- ✅ Enhanced SSE streaming
|
|
385
|
+
- ✅ BlaizeLogger as first-class citizen
|
|
386
|
+
- ✅ Correlation IDs for distributed tracing
|
|
1038
387
|
|
|
1039
|
-
|
|
1040
|
-
pnpm add -D vitest @blaizejs/testing-utils
|
|
1041
|
-
```
|
|
388
|
+
### 🎯 v1.0 (Stable Release)
|
|
1042
389
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
390
|
+
- [ ] Rate limiting plugin
|
|
391
|
+
- [ ] Compression middleware
|
|
392
|
+
- [ ] Database plugin with migrations
|
|
393
|
+
- [ ] Storage plugin (S3, local)
|
|
394
|
+
- [ ] OpenAPI/Swagger generation
|
|
395
|
+
- [ ] Authentication plugin
|
|
396
|
+
- [ ] Production deployment guides
|
|
1046
397
|
|
|
1047
|
-
|
|
1048
|
-
test: {
|
|
1049
|
-
globals: true,
|
|
1050
|
-
},
|
|
1051
|
-
});
|
|
1052
|
-
```
|
|
398
|
+
### 🔮 Future (Post-1.0)
|
|
1053
399
|
|
|
1054
|
-
|
|
400
|
+
- [ ] Edge runtime support
|
|
401
|
+
- [ ] WebSocket support (bidirectional real-time)
|
|
402
|
+
- [ ] External queue workers
|
|
403
|
+
- [ ] GraphQL integration
|
|
404
|
+
- [ ] gRPC-Web support
|
|
405
|
+
- [ ] Distributed tracing (OpenTelemetry)
|
|
406
|
+
- [ ] AI-powered route generation
|
|
1055
407
|
|
|
1056
|
-
|
|
1057
|
-
import { describe, it, expect } from 'vitest';
|
|
1058
|
-
import { createTestContext, createMockLogger } from '@blaizejs/testing-utils';
|
|
1059
|
-
import { GET } from './routes/users/[userId]';
|
|
1060
|
-
|
|
1061
|
-
describe('GET /users/:userId', () => {
|
|
1062
|
-
it('returns user by id', async () => {
|
|
1063
|
-
const ctx = createTestContext({
|
|
1064
|
-
method: 'GET',
|
|
1065
|
-
path: '/users/123',
|
|
1066
|
-
params: { userId: '123' },
|
|
1067
|
-
});
|
|
1068
|
-
const logger = createMockLogger();
|
|
1069
|
-
|
|
1070
|
-
const result = await GET.handler(ctx, { userId: '123' }, logger);
|
|
1071
|
-
|
|
1072
|
-
expect(result.id).toBe('123');
|
|
1073
|
-
expect(result.name).toBeDefined();
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
it('throws NotFoundError for missing user', async () => {
|
|
1077
|
-
const ctx = createTestContext({
|
|
1078
|
-
params: { userId: 'nonexistent' },
|
|
1079
|
-
});
|
|
1080
|
-
const logger = createMockLogger();
|
|
1081
|
-
|
|
1082
|
-
await expect(
|
|
1083
|
-
GET.handler(ctx, { userId: 'nonexistent' }, logger)
|
|
1084
|
-
).rejects.toThrow(NotFoundError);
|
|
1085
|
-
});
|
|
1086
|
-
});
|
|
1087
|
-
```
|
|
408
|
+
[View full roadmap →](https://github.com/jleajones/blaize/projects)
|
|
1088
409
|
|
|
1089
|
-
|
|
410
|
+
---
|
|
1090
411
|
|
|
1091
|
-
|
|
1092
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
1093
|
-
import { createTestContext } from '@blaizejs/testing-utils';
|
|
1094
|
-
import { authMiddleware } from './middleware/auth';
|
|
1095
|
-
|
|
1096
|
-
describe('authMiddleware', () => {
|
|
1097
|
-
it('adds user to state when token is valid', async () => {
|
|
1098
|
-
const ctx = createTestContext({
|
|
1099
|
-
headers: { authorization: 'Bearer valid-token' },
|
|
1100
|
-
});
|
|
1101
|
-
const next = vi.fn();
|
|
1102
|
-
|
|
1103
|
-
await authMiddleware.handler(ctx, next);
|
|
1104
|
-
|
|
1105
|
-
expect(ctx.state.user).toBeDefined();
|
|
1106
|
-
expect(ctx.state.user.id).toBe('user-123');
|
|
1107
|
-
expect(next).toHaveBeenCalled();
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
it('throws UnauthorizedError when token is missing', async () => {
|
|
1111
|
-
const ctx = createTestContext();
|
|
1112
|
-
const next = vi.fn();
|
|
1113
|
-
|
|
1114
|
-
await expect(
|
|
1115
|
-
authMiddleware.handler(ctx, next)
|
|
1116
|
-
).rejects.toThrow(UnauthorizedError);
|
|
1117
|
-
|
|
1118
|
-
expect(next).not.toHaveBeenCalled();
|
|
1119
|
-
});
|
|
1120
|
-
});
|
|
1121
|
-
```
|
|
412
|
+
## 🌟 Key Features
|
|
1122
413
|
|
|
1123
|
-
###
|
|
414
|
+
### Type Safety Everywhere
|
|
1124
415
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
describe('POST /users', () => {
|
|
1131
|
-
it('creates user with mocked database', async () => {
|
|
1132
|
-
const mockDb = {
|
|
1133
|
-
users: {
|
|
1134
|
-
create: vi.fn().mockResolvedValue({
|
|
1135
|
-
id: 'new-user-123',
|
|
1136
|
-
name: 'John',
|
|
1137
|
-
email: 'john@example.com',
|
|
1138
|
-
}),
|
|
1139
|
-
},
|
|
1140
|
-
};
|
|
1141
|
-
|
|
1142
|
-
const ctx = createTestContext({
|
|
1143
|
-
method: 'POST',
|
|
1144
|
-
body: { name: 'John', email: 'john@example.com' },
|
|
1145
|
-
services: { db: mockDb },
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
const result = await POST.handler(ctx, {}, createMockLogger());
|
|
1149
|
-
|
|
1150
|
-
expect(result.id).toBe('new-user-123');
|
|
1151
|
-
expect(mockDb.users.create).toHaveBeenCalledWith({
|
|
1152
|
-
name: 'John',
|
|
1153
|
-
email: 'john@example.com',
|
|
1154
|
-
});
|
|
1155
|
-
});
|
|
1156
|
-
});
|
|
1157
|
-
```
|
|
416
|
+
- **Zero runtime overhead** — Types are compile-time only
|
|
417
|
+
- **No code generation** — Pure TypeScript inference
|
|
418
|
+
- **Zod integration** — Runtime validation + type inference
|
|
419
|
+
- **End-to-end types** — Server schemas → Client calls
|
|
1158
420
|
|
|
1159
|
-
###
|
|
421
|
+
### Developer Experience
|
|
1160
422
|
|
|
1161
|
-
|
|
1162
|
-
|
|
423
|
+
- **File-based routing** — URL structure mirrors your files
|
|
424
|
+
- **Hot reload** — Changes reflect instantly
|
|
425
|
+
- **Full autocomplete** — Your IDE knows everything
|
|
426
|
+
- **Clear errors** — Helpful error messages with suggestions
|
|
1163
427
|
|
|
1164
|
-
|
|
428
|
+
### Production Ready
|
|
1165
429
|
|
|
1166
|
-
|
|
1167
|
-
|
|
430
|
+
- **HTTP/2 native** — Better performance by default
|
|
431
|
+
- **Correlation IDs** — Track requests across services
|
|
432
|
+
- **Structured logging** — BlaizeLogger with context
|
|
433
|
+
- **Semantic errors** — 12 error types with automatic HTTP formatting
|
|
434
|
+
- **Redis adapters** — Production-ready queue & cache
|
|
1168
435
|
|
|
1169
|
-
|
|
1170
|
-
expect(logger.logs).toContainEqual({
|
|
1171
|
-
level: 'info',
|
|
1172
|
-
message: 'User created',
|
|
1173
|
-
meta: expect.objectContaining({ userId: '123' }),
|
|
1174
|
-
});
|
|
1175
|
-
```
|
|
436
|
+
### Real-Time & Background Jobs
|
|
1176
437
|
|
|
1177
|
-
|
|
438
|
+
- **Server-Sent Events** — Type-safe streaming
|
|
439
|
+
- **Event Bus** — Distributed coordination
|
|
440
|
+
- **Job queues** — Priority scheduling with retries
|
|
441
|
+
- **Progress tracking** — Stream job updates via SSE
|
|
1178
442
|
|
|
1179
443
|
---
|
|
1180
444
|
|
|
1181
|
-
##
|
|
445
|
+
## 🤝 Contributing
|
|
1182
446
|
|
|
1183
|
-
|
|
447
|
+
We welcome contributions! BlaizeJS is built by developers, for developers.
|
|
1184
448
|
|
|
1185
|
-
-
|
|
1186
|
-
-
|
|
1187
|
-
-
|
|
1188
|
-
- [ ] Database plugin with migrations (`@blaizejs/plugin-db`)
|
|
1189
|
-
- [ ] Storage plugin (`@blaizejs/plugin-storage`)
|
|
1190
|
-
- [ ] OpenAPI/Swagger generation
|
|
449
|
+
- 🐛 **Found a bug?** [Open an issue](https://github.com/jleajones/blaize/issues)
|
|
450
|
+
- 💡 **Have an idea?** [Start a discussion](https://github.com/jleajones/blaize/discussions)
|
|
451
|
+
- 🔧 **Want to contribute?** See our [Contributing Guide](./CONTRIBUTING.md)
|
|
1191
452
|
|
|
1192
|
-
###
|
|
453
|
+
### Development Setup
|
|
1193
454
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
455
|
+
```bash
|
|
456
|
+
git clone https://github.com/jleajones/blaize.git
|
|
457
|
+
cd blaize
|
|
458
|
+
pnpm install
|
|
459
|
+
pnpm test
|
|
460
|
+
pnpm build
|
|
461
|
+
```
|
|
1199
462
|
|
|
1200
463
|
---
|
|
1201
464
|
|
|
1202
|
-
##
|
|
465
|
+
## 📄 License
|
|
1203
466
|
|
|
1204
|
-
|
|
1205
|
-
- [`@blaizejs/testing-utils`](../blaize-testing-utils/README.md) — Testing utilities
|
|
1206
|
-
- [Architecture Guide](../../docs/ARCHITECTURE.md) — How BlaizeJS works
|
|
1207
|
-
- [Getting Started](../../docs/GETTING-STARTED.md) — Build your first project
|
|
467
|
+
MIT © [BlaizeJS Contributors](https://github.com/jleajones/blaize/graphs/contributors)
|
|
1208
468
|
|
|
1209
469
|
---
|
|
1210
470
|
|
|
1211
|
-
##
|
|
471
|
+
## 🙏 Acknowledgments
|
|
1212
472
|
|
|
1213
|
-
|
|
473
|
+
BlaizeJS stands on the shoulders of giants:
|
|
474
|
+
|
|
475
|
+
- **[tRPC](https://trpc.io/)** — Inspiration for type-safe RPC
|
|
476
|
+
- **[Hono](https://hono.dev/)** — Influence on middleware patterns
|
|
477
|
+
- **[Fastify](https://fastify.io/)** — Performance benchmarks
|
|
478
|
+
- **[Zod](https://zod.dev/)** — Schema validation excellence
|
|
1214
479
|
|
|
1215
480
|
---
|
|
1216
481
|
|
|
1217
|
-
|
|
482
|
+
<p align="center">
|
|
483
|
+
<strong>Built with ❤️ by the BlaizeJS team</strong>
|
|
484
|
+
<br>
|
|
485
|
+
<a href="https://github.com/jleajones/blaize">GitHub</a> •
|
|
486
|
+
<a href="https://discord.gg/blaizejs">Discord</a> •
|
|
487
|
+
<a href="https://twitter.com/blaizejs">Twitter</a>
|
|
488
|
+
</p>
|