blaizejs 0.7.1 → 0.9.0
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 +1021 -476
- package/dist/chunk-6K6GZLDJ.js +11 -0
- package/dist/chunk-6K6GZLDJ.js.map +1 -0
- package/dist/chunk-DCZPGNAM.js +11 -0
- package/dist/chunk-LL6TU2VN.js +11 -0
- package/dist/chunk-PT3XLVQL.js +11 -0
- package/dist/chunk-VNJJ5PCH.js +11 -0
- package/dist/index.cjs +18 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3616 -1366
- package/dist/index.d.ts +3616 -1366
- package/dist/index.js +18 -17
- package/dist/index.js.map +1 -1
- package/dist/{internal-server-error-3NM7IWUS.js → internal-server-error-SUR4JYIM.js} +4 -4
- package/dist/{payload-too-large-error-WI42VGZU.js → payload-too-large-error-V5D4UEOE.js} +4 -4
- package/dist/{unsupported-media-type-error-TA5IO6FC.js → unsupported-media-type-error-Q2DH3CAM.js} +4 -4
- package/dist/{validation-error-NO2FE2IP.js → validation-error-QMX5LAON.js} +4 -4
- package/package.json +2 -2
- package/dist/chunk-DCFSCRSA.js +0 -11
- package/dist/chunk-EAXYRQ3P.js +0 -11
- package/dist/chunk-FCV6YEV5.js +0 -11
- package/dist/chunk-FZ7VQAU3.js +0 -11
- package/dist/chunk-FZ7VQAU3.js.map +0 -1
- package/dist/chunk-O2UQAGER.js +0 -11
- /package/dist/{chunk-FCV6YEV5.js.map → chunk-DCZPGNAM.js.map} +0 -0
- /package/dist/{chunk-DCFSCRSA.js.map → chunk-LL6TU2VN.js.map} +0 -0
- /package/dist/{chunk-O2UQAGER.js.map → chunk-PT3XLVQL.js.map} +0 -0
- /package/dist/{chunk-EAXYRQ3P.js.map → chunk-VNJJ5PCH.js.map} +0 -0
- /package/dist/{internal-server-error-3NM7IWUS.js.map → internal-server-error-SUR4JYIM.js.map} +0 -0
- /package/dist/{payload-too-large-error-WI42VGZU.js.map → payload-too-large-error-V5D4UEOE.js.map} +0 -0
- /package/dist/{unsupported-media-type-error-TA5IO6FC.js.map → unsupported-media-type-error-Q2DH3CAM.js.map} +0 -0
- /package/dist/{validation-error-NO2FE2IP.js.map → validation-error-QMX5LAON.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,672 +1,1217 @@
|
|
|
1
1
|
# 🔥 BlaizeJS Core
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The core BlaizeJS framework — type-safe APIs with file-based routing
|
|
4
4
|
|
|
5
5
|
[](https://badge.fury.io/js/blaizejs)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](https://github.com/jleajones/blaize/actions)
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
The `blaizejs` package is the core framework providing servers, routing, middleware, plugins, and error handling with end-to-end type safety.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
- [📦 Installation](#-installation)
|
|
13
|
-
- [🚀 Quick Start](#-quick-start)
|
|
14
|
-
- [📖 Core Modules](#-core-modules)
|
|
15
|
-
- [🛡️ Error Handling](#️-error-handling)
|
|
16
|
-
- [🎯 API Reference](#-api-reference)
|
|
17
|
-
- [💡 Common Patterns](#-common-patterns)
|
|
18
|
-
- [🧪 Testing](#-testing)
|
|
19
|
-
- [📚 Type System](#-type-system)
|
|
20
|
-
- [🗺️ Roadmap](#️-roadmap)
|
|
21
|
-
- [🤝 Contributing](#-contributing)
|
|
22
|
-
|
|
23
|
-
## 🌟 Features
|
|
24
|
-
|
|
25
|
-
- 🚀 **HTTP/2 by Default** - Modern protocol with automatic HTTPS in development
|
|
26
|
-
- 📁 **File-Based Routing** - Routes auto-discovered from file structure *(internal)*
|
|
27
|
-
- 🔧 **Composable Middleware** - Build reusable request/response pipelines
|
|
28
|
-
- 🧩 **Plugin System** - Extend server functionality with lifecycle hooks
|
|
29
|
-
- ✅ **Schema Validation** - Built-in Zod validation for type safety
|
|
30
|
-
- 🛡️ **Semantic Errors** - Rich error classes with automatic formatting
|
|
31
|
-
- 🔗 **Context Management** - AsyncLocalStorage-powered state isolation *(internal)*
|
|
32
|
-
- ⚡ **Zero Configuration** - Works out of the box with sensible defaults
|
|
33
|
-
- 📊 **Type Inference** - Full TypeScript support with automatic types
|
|
34
|
-
- 🔄 **Hot Reloading** - Development mode with automatic route updates
|
|
13
|
+
---
|
|
35
14
|
|
|
36
15
|
## 📦 Installation
|
|
37
16
|
|
|
17
|
+
### Recommended: Create a New Project
|
|
18
|
+
|
|
38
19
|
```bash
|
|
39
20
|
# Using pnpm (recommended)
|
|
40
|
-
pnpm
|
|
21
|
+
pnpm dlx create-blaize-app my-app
|
|
41
22
|
|
|
42
23
|
# Using npm
|
|
43
|
-
|
|
24
|
+
npx create-blaize-app my-app
|
|
44
25
|
|
|
45
26
|
# Using yarn
|
|
46
|
-
yarn
|
|
27
|
+
yarn dlx create-blaize-app my-app
|
|
47
28
|
```
|
|
48
29
|
|
|
49
|
-
|
|
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
|
+
---
|
|
50
48
|
|
|
51
|
-
|
|
49
|
+
## 🚀 Quick Start
|
|
52
50
|
|
|
53
51
|
```typescript
|
|
54
|
-
|
|
52
|
+
// src/app.ts
|
|
53
|
+
import { Blaize, type InferContext } from 'blaizejs';
|
|
55
54
|
import { fileURLToPath } from 'node:url';
|
|
56
55
|
import path from 'node:path';
|
|
57
|
-
import { z } from 'zod';
|
|
58
56
|
|
|
59
|
-
// ESM path resolution (required for route discovery)
|
|
60
57
|
const __filename = fileURLToPath(import.meta.url);
|
|
61
58
|
const __dirname = path.dirname(__filename);
|
|
62
59
|
|
|
63
|
-
|
|
60
|
+
const app = Blaize.createServer({
|
|
61
|
+
port: 3000,
|
|
62
|
+
routesDir: path.resolve(__dirname, './routes'),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Create a typed route factory for use in route files
|
|
66
|
+
type AppContext = InferContext;
|
|
67
|
+
export const route = Blaize.Router.createRouteFactory<
|
|
68
|
+
AppContext['state'],
|
|
69
|
+
AppContext['services']
|
|
70
|
+
>();
|
|
71
|
+
|
|
72
|
+
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
|
+
```
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// src/app-type.ts — Export routes registry for the client
|
|
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
|
+
|
|
64
167
|
const server = createServer({
|
|
65
168
|
port: 3000,
|
|
66
|
-
|
|
67
|
-
routesDir: path.resolve(__dirname, './routes')
|
|
169
|
+
routesDir: './src/routes',
|
|
68
170
|
});
|
|
69
171
|
|
|
70
172
|
await server.listen();
|
|
71
|
-
console.log(`🚀 Server running at https://localhost:3000`);
|
|
72
173
|
```
|
|
73
174
|
|
|
74
|
-
|
|
175
|
+
**With Middleware:**
|
|
75
176
|
|
|
76
|
-
|
|
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:**
|
|
77
211
|
|
|
78
212
|
```typescript
|
|
79
|
-
|
|
80
|
-
|
|
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';
|
|
81
278
|
import { z } from 'zod';
|
|
82
279
|
|
|
83
|
-
//
|
|
84
|
-
export const
|
|
280
|
+
// Named exports — these names become the client method names
|
|
281
|
+
export const listUsers = route.get({
|
|
85
282
|
schema: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
});
|
|
94
292
|
},
|
|
95
|
-
handler: async (ctx, params) => {
|
|
96
|
-
const user = await db.users.findById(params.userId);
|
|
97
|
-
|
|
98
|
-
if (!user) {
|
|
99
|
-
throw new NotFoundError('User not found', {
|
|
100
|
-
resourceType: 'user',
|
|
101
|
-
resourceId: params.userId
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return user;
|
|
106
|
-
}
|
|
107
293
|
});
|
|
108
294
|
|
|
109
|
-
|
|
110
|
-
export const PUT = createPutRoute({
|
|
295
|
+
export const createUser = route.post({
|
|
111
296
|
schema: {
|
|
112
|
-
params: z.object({
|
|
113
|
-
userId: z.string().uuid()
|
|
114
|
-
}),
|
|
115
297
|
body: z.object({
|
|
116
298
|
name: z.string().min(1),
|
|
117
|
-
email: z.string().email()
|
|
118
|
-
})
|
|
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);
|
|
119
305
|
},
|
|
120
|
-
handler: async (ctx, params) => {
|
|
121
|
-
const updatedUser = await db.users.update(params.userId, ctx.body);
|
|
122
|
-
return updatedUser;
|
|
123
|
-
}
|
|
124
306
|
});
|
|
125
307
|
```
|
|
126
308
|
|
|
127
|
-
|
|
309
|
+
```typescript
|
|
310
|
+
// src/routes/users/[userId].ts
|
|
311
|
+
import { route } from '../../app';
|
|
312
|
+
import { z } from 'zod';
|
|
313
|
+
import { NotFoundError, ForbiddenError } from 'blaizejs';
|
|
128
314
|
|
|
129
|
-
|
|
315
|
+
export const getUser = route.get({
|
|
316
|
+
schema: {
|
|
317
|
+
params: z.object({ userId: z.string().uuid() }),
|
|
318
|
+
response: userSchema,
|
|
319
|
+
},
|
|
320
|
+
handler: async (ctx, params) => {
|
|
321
|
+
const user = await ctx.services.db.users.findById(params.userId);
|
|
322
|
+
if (!user) throw new NotFoundError('User not found');
|
|
323
|
+
return user;
|
|
324
|
+
},
|
|
325
|
+
});
|
|
130
326
|
|
|
131
|
-
|
|
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
|
+
});
|
|
132
341
|
|
|
133
|
-
|
|
342
|
+
export const deleteUser = route.delete({
|
|
343
|
+
schema: {
|
|
344
|
+
params: z.object({ userId: z.string() }),
|
|
345
|
+
},
|
|
346
|
+
handler: async (ctx, params) => {
|
|
347
|
+
await ctx.services.db.users.delete(params.userId);
|
|
348
|
+
ctx.response.status(204);
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
```
|
|
134
352
|
|
|
135
353
|
```typescript
|
|
136
|
-
|
|
354
|
+
// src/app-type.ts — Export routes registry for the client
|
|
355
|
+
import { listUsers, createUser } from './routes/users';
|
|
356
|
+
import { getUser, updateUser, deleteUser } from './routes/users/[userId]';
|
|
357
|
+
|
|
358
|
+
export const routes = {
|
|
359
|
+
listUsers,
|
|
360
|
+
createUser,
|
|
361
|
+
getUser,
|
|
362
|
+
updateUser,
|
|
363
|
+
deleteUser,
|
|
364
|
+
} as const;
|
|
365
|
+
```
|
|
137
366
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
367
|
+
#### SSE Routes with the Factory
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// src/routes/jobs/[jobId]/stream.ts
|
|
371
|
+
import { route } from '../../../app';
|
|
372
|
+
import { z } from 'zod';
|
|
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
|
+
},
|
|
144
398
|
});
|
|
145
399
|
|
|
146
|
-
//
|
|
147
|
-
|
|
400
|
+
// Client usage:
|
|
401
|
+
// const stream = await client.$sse.getJobStream({ params: { jobId: '123' } });
|
|
402
|
+
// stream.on('progress', (data) => console.log(data.percent));
|
|
403
|
+
```
|
|
148
404
|
|
|
149
|
-
|
|
150
|
-
await server.register(databasePlugin());
|
|
405
|
+
**SSE Handler Signature:**
|
|
151
406
|
|
|
152
|
-
|
|
407
|
+
```typescript
|
|
408
|
+
handler: (
|
|
409
|
+
stream: TypedSSEStream, // Send typed events
|
|
410
|
+
ctx: Context, // Request context
|
|
411
|
+
params: TParams, // Validated parameters
|
|
412
|
+
logger: BlaizeLogger // Request-scoped logger
|
|
413
|
+
) => Promise
|
|
153
414
|
```
|
|
154
415
|
|
|
155
|
-
|
|
416
|
+
**TypedSSEStream Methods:**
|
|
417
|
+
|
|
418
|
+
| Method | Description |
|
|
419
|
+
|--------|-------------|
|
|
420
|
+
| `send(event, data)` | Send a typed event to the client |
|
|
421
|
+
| `close()` | Close the SSE connection |
|
|
156
422
|
|
|
157
|
-
|
|
423
|
+
---
|
|
158
424
|
|
|
159
|
-
|
|
160
|
-
- ✅ `createGetRoute` - Create GET endpoints
|
|
161
|
-
- ✅ `createPostRoute` - Create POST endpoints
|
|
162
|
-
- ✅ `createPutRoute` - Create PUT endpoints
|
|
163
|
-
- ✅ `createPatchRoute` - Create PATCH endpoints
|
|
164
|
-
- ✅ `createDeleteRoute` - Create DELETE endpoints
|
|
165
|
-
- ✅ `createHeadRoute` - Create HEAD endpoints
|
|
166
|
-
- ✅ `createOptionsRoute` - Create OPTIONS endpoints
|
|
425
|
+
### Individual Route Creators
|
|
167
426
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
- ❌ `extractParams` - Internal parameter extraction
|
|
172
|
-
- ❌ Route discovery utilities - Internal file system operations
|
|
427
|
+
For cases where you don't need shared context types, or when building custom abstractions, you can use the individual route creator functions directly.
|
|
428
|
+
|
|
429
|
+
> **Note:** These are higher-order functions (they return functions). The route factory pattern above is recommended for most applications.
|
|
173
430
|
|
|
174
431
|
```typescript
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
432
|
+
import {
|
|
433
|
+
createGetRoute,
|
|
434
|
+
createPostRoute,
|
|
435
|
+
createPutRoute,
|
|
436
|
+
createPatchRoute,
|
|
437
|
+
createDeleteRoute,
|
|
438
|
+
createHeadRoute,
|
|
439
|
+
createOptionsRoute,
|
|
440
|
+
createSSERoute,
|
|
441
|
+
} from 'blaizejs';
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### Example: Using Individual Route Creators
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
import { createGetRoute } from 'blaizejs';
|
|
448
|
+
import { z } from 'zod';
|
|
180
449
|
|
|
181
|
-
//
|
|
182
|
-
|
|
450
|
+
// Note: createGetRoute() returns a function
|
|
451
|
+
const getRoute = createGetRoute();
|
|
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
|
+
},
|
|
464
|
+
});
|
|
183
465
|
```
|
|
184
466
|
|
|
185
|
-
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 🔗 Middleware
|
|
470
|
+
|
|
471
|
+
### createMiddleware
|
|
186
472
|
|
|
187
|
-
|
|
473
|
+
Create middleware with typed state and service additions.
|
|
188
474
|
|
|
189
475
|
```typescript
|
|
190
|
-
import { createMiddleware
|
|
476
|
+
import { createMiddleware } from 'blaizejs';
|
|
477
|
+
|
|
478
|
+
const middleware = createMiddleware({
|
|
479
|
+
name?: string;
|
|
480
|
+
handler: (ctx: Context, next: NextFunction) => Promise;
|
|
481
|
+
skip?: (ctx: Context) => boolean;
|
|
482
|
+
debug?: boolean;
|
|
483
|
+
});
|
|
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
|
+
|
|
495
|
+
#### Type Parameters
|
|
496
|
+
|
|
497
|
+
| Parameter | Description |
|
|
498
|
+
|-----------|-------------|
|
|
499
|
+
| `TState` | Properties added to `ctx.state` |
|
|
500
|
+
| `TServices` | Properties added to `ctx.services` |
|
|
191
501
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
502
|
+
#### Examples
|
|
503
|
+
|
|
504
|
+
**Logging Middleware:**
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
const loggingMiddleware = createMiddleware({
|
|
508
|
+
name: 'logger',
|
|
509
|
+
handler: async (ctx, next) => {
|
|
510
|
+
const start = Date.now();
|
|
511
|
+
console.log(`→ ${ctx.request.method} ${ctx.request.path}`);
|
|
512
|
+
|
|
513
|
+
await next();
|
|
514
|
+
|
|
515
|
+
const duration = Date.now() - start;
|
|
516
|
+
console.log(`← ${ctx.response.statusCode} (${duration}ms)`);
|
|
517
|
+
},
|
|
197
518
|
});
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**Authentication Middleware:**
|
|
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
|
+
}
|
|
198
533
|
|
|
199
|
-
|
|
200
|
-
|
|
534
|
+
const authMiddleware = createMiddleware<
|
|
535
|
+
{ user: User },
|
|
536
|
+
{ auth: AuthService }
|
|
537
|
+
>({
|
|
201
538
|
name: 'auth',
|
|
202
539
|
handler: async (ctx, next) => {
|
|
203
|
-
const token = ctx.request.header('authorization');
|
|
540
|
+
const token = ctx.request.header('authorization')?.replace('Bearer ', '');
|
|
541
|
+
|
|
204
542
|
if (!token) {
|
|
205
|
-
throw new UnauthorizedError('
|
|
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');
|
|
206
551
|
}
|
|
207
|
-
|
|
552
|
+
|
|
208
553
|
await next();
|
|
209
554
|
},
|
|
210
|
-
skip: ctx => ctx.request.path
|
|
555
|
+
skip: (ctx) => ctx.request.path === '/health',
|
|
211
556
|
});
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Timing Middleware:**
|
|
212
560
|
|
|
213
|
-
|
|
214
|
-
const
|
|
561
|
+
```typescript
|
|
562
|
+
const timingMiddleware = createMiddleware({
|
|
563
|
+
name: 'timing',
|
|
564
|
+
handler: async (ctx, next) => {
|
|
565
|
+
ctx.state.requestStart = Date.now();
|
|
566
|
+
await next();
|
|
567
|
+
|
|
568
|
+
const duration = Date.now() - ctx.state.requestStart;
|
|
569
|
+
ctx.response.header('X-Response-Time', `${duration}ms`);
|
|
570
|
+
},
|
|
571
|
+
});
|
|
215
572
|
```
|
|
216
573
|
|
|
217
|
-
###
|
|
574
|
+
### createStateMiddleware
|
|
218
575
|
|
|
219
|
-
|
|
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
|
|
590
|
+
|
|
591
|
+
Shorthand for middleware that only adds services.
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
import { createServiceMiddleware } from 'blaizejs';
|
|
595
|
+
|
|
596
|
+
const dbMiddleware = createServiceMiddleware(
|
|
597
|
+
async (ctx, next) => {
|
|
598
|
+
ctx.services.db = database;
|
|
599
|
+
await next();
|
|
600
|
+
}
|
|
601
|
+
);
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### compose
|
|
605
|
+
|
|
606
|
+
Combine multiple middleware into a single middleware.
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
import { compose } from 'blaizejs';
|
|
610
|
+
|
|
611
|
+
const combined = compose([
|
|
612
|
+
loggingMiddleware,
|
|
613
|
+
authMiddleware,
|
|
614
|
+
timingMiddleware,
|
|
615
|
+
]);
|
|
616
|
+
|
|
617
|
+
const server = createServer({
|
|
618
|
+
middleware: [combined],
|
|
619
|
+
routesDir: './routes',
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## 🔌 Plugins
|
|
626
|
+
|
|
627
|
+
### createPlugin
|
|
628
|
+
|
|
629
|
+
Create a plugin with lifecycle hooks and service injection.
|
|
220
630
|
|
|
221
631
|
```typescript
|
|
222
632
|
import { createPlugin } from 'blaizejs';
|
|
223
633
|
|
|
224
|
-
const
|
|
225
|
-
|
|
634
|
+
const plugin = createPlugin(
|
|
635
|
+
name: string,
|
|
636
|
+
version: string,
|
|
637
|
+
setup: (server: Server, options: TOptions) => void | PluginHooks | Promise,
|
|
638
|
+
defaultOptions?: Partial
|
|
639
|
+
);
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
#### Parameters
|
|
643
|
+
|
|
644
|
+
| Parameter | Type | Description |
|
|
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 |
|
|
650
|
+
|
|
651
|
+
#### Plugin Hooks
|
|
652
|
+
|
|
653
|
+
| Hook | When | Use Case |
|
|
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 |
|
|
660
|
+
|
|
661
|
+
#### Examples
|
|
662
|
+
|
|
663
|
+
**Simple Plugin:**
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
const helloPlugin = createPlugin(
|
|
667
|
+
'hello',
|
|
226
668
|
'1.0.0',
|
|
227
669
|
(server) => {
|
|
228
|
-
|
|
670
|
+
console.log('Hello plugin registered!');
|
|
229
671
|
|
|
230
672
|
return {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
console.log('Database connected');
|
|
234
|
-
},
|
|
235
|
-
onServerStart: async () => {
|
|
236
|
-
await connection.migrate();
|
|
673
|
+
onServerStart: () => {
|
|
674
|
+
console.log('Server started!');
|
|
237
675
|
},
|
|
238
|
-
onServerStop: async () => {
|
|
239
|
-
await connection.close();
|
|
240
|
-
},
|
|
241
|
-
terminate: async () => {
|
|
242
|
-
console.log('Database plugin terminated');
|
|
243
|
-
}
|
|
244
676
|
};
|
|
245
677
|
}
|
|
246
678
|
);
|
|
247
679
|
|
|
248
|
-
// Use in server
|
|
249
680
|
const server = createServer({
|
|
250
|
-
plugins: [
|
|
681
|
+
plugins: [helloPlugin()],
|
|
251
682
|
});
|
|
252
683
|
```
|
|
253
684
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
**⚠️ Note**: Context is automatically managed. You interact with it in handlers.
|
|
257
|
-
|
|
258
|
-
Context is automatically provided to all route handlers and middleware:
|
|
685
|
+
**Plugin with Services:**
|
|
259
686
|
|
|
260
687
|
```typescript
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
688
|
+
interface DatabaseOptions {
|
|
689
|
+
connectionString: string;
|
|
690
|
+
poolSize?: number;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const databasePlugin = createPlugin(
|
|
694
|
+
'database',
|
|
695
|
+
'1.0.0',
|
|
696
|
+
(server, options) => {
|
|
697
|
+
let db: Database;
|
|
270
698
|
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}));
|
|
274
707
|
|
|
275
|
-
return {
|
|
276
|
-
|
|
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
|
+
],
|
|
277
729
|
});
|
|
278
730
|
```
|
|
279
731
|
|
|
280
|
-
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## ⚠️ Error Classes
|
|
735
|
+
|
|
736
|
+
BlaizeJS provides 12 semantic error classes that automatically format to HTTP responses.
|
|
737
|
+
|
|
738
|
+
### Error Response Format
|
|
739
|
+
|
|
740
|
+
All errors produce this response structure:
|
|
281
741
|
|
|
282
|
-
|
|
742
|
+
```json
|
|
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
|
+
}
|
|
751
|
+
```
|
|
283
752
|
|
|
284
|
-
|
|
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
|
|
285
771
|
|
|
286
772
|
```typescript
|
|
287
773
|
import {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
PayloadTooLargeError, // 413 - Payload Too Large
|
|
295
|
-
UnsupportedMediaTypeError, // 415 - Unsupported Media Type
|
|
296
|
-
UnprocessableEntityError, // 422 - Unprocessable Entity
|
|
297
|
-
RateLimitError, // 429 - Too Many Requests
|
|
298
|
-
InternalServerError // 500 - Server Error
|
|
774
|
+
NotFoundError,
|
|
775
|
+
ValidationError,
|
|
776
|
+
UnauthorizedError,
|
|
777
|
+
ForbiddenError,
|
|
778
|
+
ConflictError,
|
|
779
|
+
RateLimitError,
|
|
299
780
|
} from 'blaizejs';
|
|
300
781
|
|
|
301
|
-
//
|
|
782
|
+
// Basic usage
|
|
783
|
+
throw new NotFoundError('User not found');
|
|
784
|
+
|
|
785
|
+
// With details
|
|
302
786
|
throw new NotFoundError('User not found', {
|
|
303
787
|
resourceType: 'user',
|
|
304
|
-
resourceId:
|
|
305
|
-
suggestion: '
|
|
788
|
+
resourceId: userId,
|
|
789
|
+
suggestion: 'Verify the user ID exists',
|
|
306
790
|
});
|
|
307
791
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
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
|
+
});
|
|
799
|
+
|
|
800
|
+
// Rate limit with retry info
|
|
801
|
+
throw new RateLimitError('Too many requests', {
|
|
802
|
+
retryAfter: 60,
|
|
803
|
+
limit: 100,
|
|
804
|
+
remaining: 0,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// Conflict with version info
|
|
808
|
+
throw new ConflictError('Version mismatch', {
|
|
809
|
+
currentVersion: 5,
|
|
810
|
+
providedVersion: 3,
|
|
811
|
+
});
|
|
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');
|
|
317
819
|
```
|
|
318
820
|
|
|
319
|
-
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
## 📝 Logging
|
|
320
824
|
|
|
321
|
-
|
|
825
|
+
### Global Logger
|
|
322
826
|
|
|
323
827
|
```typescript
|
|
324
|
-
import {
|
|
325
|
-
PayloadTooLargeError, // 413 - Request Entity Too Large
|
|
326
|
-
UnsupportedMediaTypeError, // 415 - Unsupported Media Type
|
|
327
|
-
RequestTimeoutError, // 408 - Request Timeout
|
|
328
|
-
UnprocessableEntityError // 422 - Unprocessable Entity
|
|
329
|
-
} from 'blaizejs';
|
|
828
|
+
import { logger } from 'blaizejs';
|
|
330
829
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
filename: 'huge-video.mp4',
|
|
336
|
-
currentSize: 104857600, // 100MB
|
|
337
|
-
maxSize: 52428800 // 50MB
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Wrong content type
|
|
341
|
-
throw new UnsupportedMediaTypeError('File type not allowed', {
|
|
342
|
-
receivedMimeType: 'application/x-executable',
|
|
343
|
-
allowedMimeTypes: ['image/jpeg', 'image/png', 'application/pdf'],
|
|
344
|
-
filename: 'virus.exe'
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// Request timeout
|
|
348
|
-
throw new RequestTimeoutError('Upload timeout', {
|
|
349
|
-
timeoutMs: 30000,
|
|
350
|
-
elapsedMs: 31000,
|
|
351
|
-
operation: 'file-upload'
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// Business rule violation
|
|
355
|
-
throw new UnprocessableEntityError('Business rule violation', {
|
|
356
|
-
rule: 'minimum_order_amount',
|
|
357
|
-
currentValue: 5.00,
|
|
358
|
-
requiredValue: 10.00,
|
|
359
|
-
message: 'Order total must be at least $10.00'
|
|
360
|
-
});
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
## 🎯 API Reference
|
|
364
|
-
|
|
365
|
-
### Exported Functions
|
|
366
|
-
|
|
367
|
-
| Function | Description |
|
|
368
|
-
|----------|-------------|
|
|
369
|
-
| **Server** | |
|
|
370
|
-
| `createServer(options?)` | Create HTTP/2 server instance |
|
|
371
|
-
| **Routing** | |
|
|
372
|
-
| `createGetRoute(config)` | Create GET endpoint |
|
|
373
|
-
| `createPostRoute(config)` | Create POST endpoint |
|
|
374
|
-
| `createPutRoute(config)` | Create PUT endpoint |
|
|
375
|
-
| `createPatchRoute(config)` | Create PATCH endpoint |
|
|
376
|
-
| `createDeleteRoute(config)` | Create DELETE endpoint |
|
|
377
|
-
| `createHeadRoute(config)` | Create HEAD endpoint |
|
|
378
|
-
| `createOptionsRoute(config)` | Create OPTIONS endpoint |
|
|
379
|
-
| **Middleware** | |
|
|
380
|
-
| `createMiddleware(handler)` | Create middleware instance |
|
|
381
|
-
| `compose(middleware[])` | Compose multiple middleware |
|
|
382
|
-
| **Plugins** | |
|
|
383
|
-
| `createPlugin(name, version, factory)` | Create server plugin |
|
|
384
|
-
| **Errors** | |
|
|
385
|
-
| `ValidationError` | 400 Bad Request |
|
|
386
|
-
| `UnauthorizedError` | 401 Unauthorized |
|
|
387
|
-
| `ForbiddenError` | 403 Forbidden |
|
|
388
|
-
| `NotFoundError` | 404 Not Found |
|
|
389
|
-
| `RequestTimeoutError` | 408 Request Timeout |
|
|
390
|
-
| `ConflictError` | 409 Conflict |
|
|
391
|
-
| `PayloadTooLargeError` | 413 Payload Too Large |
|
|
392
|
-
| `UnsupportedMediaTypeError` | 415 Unsupported Media Type |
|
|
393
|
-
| `UnprocessableEntityError` | 422 Unprocessable Entity |
|
|
394
|
-
| `RateLimitError` | 429 Too Many Requests |
|
|
395
|
-
| `InternalServerError` | 500 Internal Server Error |
|
|
396
|
-
|
|
397
|
-
### Exported Types
|
|
398
|
-
|
|
399
|
-
All types are re-exported from `@blaize-types`:
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
import type {
|
|
403
|
-
// Server types
|
|
404
|
-
Server,
|
|
405
|
-
ServerOptionsInput,
|
|
406
|
-
|
|
407
|
-
// Middleware types
|
|
408
|
-
Middleware,
|
|
409
|
-
MiddlewareFunction,
|
|
410
|
-
MiddlewareOptions,
|
|
411
|
-
NextFunction,
|
|
412
|
-
|
|
413
|
-
// Plugin types
|
|
414
|
-
Plugin,
|
|
415
|
-
PluginFactory,
|
|
416
|
-
PluginHooks,
|
|
417
|
-
|
|
418
|
-
// Router types (limited export)
|
|
419
|
-
HttpMethod,
|
|
420
|
-
RouteHandler,
|
|
421
|
-
RouteMethodOptions,
|
|
422
|
-
|
|
423
|
-
// Context types
|
|
424
|
-
Context,
|
|
425
|
-
|
|
426
|
-
// Error types
|
|
427
|
-
BlaizeError,
|
|
428
|
-
ErrorType
|
|
429
|
-
} from 'blaizejs';
|
|
830
|
+
logger.info('Server started', { port: 3000 });
|
|
831
|
+
logger.warn('Cache miss', { key: 'user:123' });
|
|
832
|
+
logger.error('Database error', { error: err.message });
|
|
833
|
+
logger.debug('Request received', { path: '/users' });
|
|
430
834
|
```
|
|
431
835
|
|
|
432
|
-
|
|
836
|
+
### createLogger
|
|
433
837
|
|
|
434
|
-
|
|
838
|
+
Create a custom logger with specific transports.
|
|
435
839
|
|
|
436
840
|
```typescript
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
await next();
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
export const GET = createGetRoute({
|
|
450
|
-
middleware: [authMiddleware],
|
|
451
|
-
handler: async (ctx) => {
|
|
452
|
-
return { user: ctx.state.user };
|
|
453
|
-
}
|
|
841
|
+
import { createLogger, ConsoleTransport, JSONTransport } from 'blaizejs';
|
|
842
|
+
|
|
843
|
+
const customLogger = createLogger({
|
|
844
|
+
level: 'info',
|
|
845
|
+
transports: [
|
|
846
|
+
new ConsoleTransport({ colorize: true }),
|
|
847
|
+
new JSONTransport({ destination: './logs/app.log' }),
|
|
848
|
+
],
|
|
454
849
|
});
|
|
455
850
|
```
|
|
456
851
|
|
|
457
|
-
###
|
|
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:
|
|
458
863
|
|
|
459
864
|
```typescript
|
|
460
|
-
export const
|
|
865
|
+
export const getUser = route.get({
|
|
461
866
|
schema: {
|
|
462
|
-
|
|
463
|
-
email: z.string().email(),
|
|
464
|
-
password: z.string().min(8),
|
|
465
|
-
age: z.number().int().positive().optional()
|
|
466
|
-
})
|
|
867
|
+
params: z.object({ userId: z.string() }),
|
|
467
868
|
},
|
|
468
|
-
handler: async (ctx) => {
|
|
469
|
-
|
|
470
|
-
|
|
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 });
|
|
471
876
|
return user;
|
|
472
|
-
}
|
|
877
|
+
},
|
|
473
878
|
});
|
|
474
879
|
```
|
|
475
880
|
|
|
476
|
-
###
|
|
881
|
+
### configureGlobalLogger
|
|
882
|
+
|
|
883
|
+
Configure the global logger instance.
|
|
477
884
|
|
|
478
885
|
```typescript
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
resourceType: 'item',
|
|
487
|
-
resourceId: params.id
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (!hasPermission(ctx.state.user, resource)) {
|
|
492
|
-
throw new ForbiddenError('Access denied', {
|
|
493
|
-
resource: resource.id,
|
|
494
|
-
requiredPermission: 'read'
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
return resource;
|
|
499
|
-
} catch (error) {
|
|
500
|
-
// Framework automatically handles error responses
|
|
501
|
-
throw error;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
886
|
+
import { configureGlobalLogger, JSONTransport } from 'blaizejs';
|
|
887
|
+
|
|
888
|
+
configureGlobalLogger({
|
|
889
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
890
|
+
transports: [
|
|
891
|
+
new JSONTransport({ pretty: process.env.NODE_ENV !== 'production' }),
|
|
892
|
+
],
|
|
504
893
|
});
|
|
505
894
|
```
|
|
506
895
|
|
|
507
|
-
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## 🛠️ Utilities
|
|
508
899
|
|
|
509
|
-
|
|
900
|
+
### getCorrelationId
|
|
901
|
+
|
|
902
|
+
Get the current request's correlation ID (from AsyncLocalStorage).
|
|
510
903
|
|
|
511
904
|
```typescript
|
|
512
|
-
import {
|
|
513
|
-
import { describe, test, expect } from 'vitest';
|
|
905
|
+
import { getCorrelationId } from 'blaizejs';
|
|
514
906
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
907
|
+
function someDeepFunction() {
|
|
908
|
+
const correlationId = getCorrelationId();
|
|
909
|
+
console.log(`Processing request ${correlationId}`);
|
|
910
|
+
}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### cors
|
|
914
|
+
|
|
915
|
+
CORS middleware for cross-origin requests.
|
|
916
|
+
|
|
917
|
+
```typescript
|
|
918
|
+
import { createServer, cors } from 'blaizejs';
|
|
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',
|
|
531
931
|
});
|
|
532
932
|
```
|
|
533
933
|
|
|
534
|
-
|
|
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) |
|
|
535
944
|
|
|
536
|
-
|
|
945
|
+
#### Examples
|
|
537
946
|
|
|
538
|
-
|
|
947
|
+
**Multiple Origins:**
|
|
539
948
|
|
|
540
949
|
```typescript
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
body: z.object({
|
|
544
|
-
name: z.string(),
|
|
545
|
-
age: z.number()
|
|
546
|
-
}),
|
|
547
|
-
response: z.object({
|
|
548
|
-
id: z.string(),
|
|
549
|
-
created: z.boolean()
|
|
550
|
-
})
|
|
551
|
-
},
|
|
552
|
-
handler: async (ctx) => {
|
|
553
|
-
// ctx.body is typed as { name: string; age: number }
|
|
554
|
-
// Return type must match response schema
|
|
555
|
-
return {
|
|
556
|
-
id: '123',
|
|
557
|
-
created: true
|
|
558
|
-
};
|
|
559
|
-
}
|
|
950
|
+
cors({
|
|
951
|
+
origin: ['https://app.example.com', 'https://admin.example.com'],
|
|
560
952
|
});
|
|
561
953
|
```
|
|
562
954
|
|
|
563
|
-
|
|
955
|
+
**Dynamic Origin:**
|
|
564
956
|
|
|
565
957
|
```typescript
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
role: string;
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
}
|
|
958
|
+
cors({
|
|
959
|
+
origin: (origin) => {
|
|
960
|
+
return origin?.endsWith('.example.com') ?? false;
|
|
961
|
+
},
|
|
962
|
+
});
|
|
575
963
|
```
|
|
576
964
|
|
|
577
|
-
|
|
965
|
+
---
|
|
578
966
|
|
|
579
|
-
|
|
967
|
+
## 📦 Context Reference
|
|
968
|
+
|
|
969
|
+
The `Context` object is available in all route handlers and middleware.
|
|
970
|
+
|
|
971
|
+
### ctx.request
|
|
972
|
+
|
|
973
|
+
| Property/Method | Type | Description |
|
|
974
|
+
|-----------------|------|-------------|
|
|
975
|
+
| `method` | `string` | HTTP method (GET, POST, etc.) |
|
|
976
|
+
| `path` | `string` | Request path |
|
|
977
|
+
| `url` | `string \| null` | Full URL |
|
|
978
|
+
| `query` | `Record<string, unknown>` | Parsed query parameters |
|
|
979
|
+
| `params` | `Record<string, string>` | Route parameters |
|
|
980
|
+
| `body` | `unknown` | Parsed request body |
|
|
981
|
+
| `protocol` | `'http' \| 'https'` | Request protocol |
|
|
982
|
+
| `isHttp2` | `boolean` | Whether using HTTP/2 |
|
|
983
|
+
| `header(name)` | `string \| undefined` | Get a header value |
|
|
984
|
+
| `headers(names?)` | `Record<string, string>` | Get multiple headers |
|
|
985
|
+
| `raw` | `IncomingMessage` | Raw Node.js request |
|
|
986
|
+
|
|
987
|
+
### ctx.response
|
|
988
|
+
|
|
989
|
+
| Property/Method | Type | Description |
|
|
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:
|
|
580
1005
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
- ✅ Plugin architecture
|
|
585
|
-
- ✅ 11 semantic error classes (400-500 status codes)
|
|
586
|
-
- ✅ Schema validation with Zod
|
|
587
|
-
- ✅ Context management (internal)
|
|
588
|
-
- ✅ Type-safe route creation
|
|
1006
|
+
```typescript
|
|
1007
|
+
// Added by authMiddleware
|
|
1008
|
+
ctx.state.user // User object
|
|
589
1009
|
|
|
590
|
-
|
|
1010
|
+
// Added by timingMiddleware
|
|
1011
|
+
ctx.state.startTime // number
|
|
1012
|
+
```
|
|
591
1013
|
|
|
592
|
-
|
|
593
|
-
- 🔄 **Export Router Utilities** - Parameter extraction, route matching for extensions
|
|
594
|
-
- 🔄 **Custom Error Factory** - Allow user-defined error classes
|
|
595
|
-
- 🔄 **Enhanced Testing Utils** - More comprehensive testing helpers
|
|
596
|
-
- 🔄 **Performance Monitoring** - Built-in metrics and profiling
|
|
597
|
-
- 🔄 **Additional HTTP Status Codes** - 405, 502, 503, 504 error classes
|
|
1014
|
+
### ctx.services
|
|
598
1015
|
|
|
599
|
-
|
|
600
|
-
- 🔄 **WebSocket Support** - Real-time communication
|
|
601
|
-
- 🔄 **Response Helpers** - Utility functions for common responses
|
|
602
|
-
- 🔄 **Route Metadata** - Attach custom metadata to routes
|
|
603
|
-
- 🔄 **Built-in Middleware** - CORS, compression, security headers
|
|
604
|
-
- 🔄 **Request Streaming** - Handle large payloads efficiently
|
|
1016
|
+
Plugin-injected services:
|
|
605
1017
|
|
|
606
|
-
|
|
1018
|
+
```typescript
|
|
1019
|
+
// Added by databasePlugin
|
|
1020
|
+
ctx.services.db // Database instance
|
|
607
1021
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
- 🔄 **OpenAPI Generation** - Automatic API documentation
|
|
611
|
-
- 🔄 **Distributed Tracing** - OpenTelemetry integration
|
|
612
|
-
- 🔄 **Edge Runtime Support** - Cloudflare Workers, Deno Deploy
|
|
613
|
-
- 🔄 **Bun Compatibility** - Native Bun.serve integration
|
|
1022
|
+
// Added by cachePlugin
|
|
1023
|
+
ctx.services.cache // CacheService
|
|
614
1024
|
|
|
615
|
-
|
|
1025
|
+
// Added by queuePlugin
|
|
1026
|
+
ctx.services.queue // QueueService
|
|
1027
|
+
```
|
|
616
1028
|
|
|
617
|
-
|
|
1029
|
+
> 🔒 **Note:** `createContext` is internal and not exported. For testing, use `createTestContext` from `@blaizejs/testing-utils`.
|
|
618
1030
|
|
|
619
|
-
|
|
1031
|
+
---
|
|
1032
|
+
|
|
1033
|
+
## 🧪 Testing
|
|
1034
|
+
|
|
1035
|
+
BlaizeJS integrates with [Vitest](https://vitest.dev/) through the `@blaizejs/testing-utils` package.
|
|
1036
|
+
|
|
1037
|
+
### Quick Setup
|
|
620
1038
|
|
|
621
1039
|
```bash
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
1040
|
+
pnpm add -D vitest @blaizejs/testing-utils
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
```typescript
|
|
1044
|
+
// vitest.config.ts
|
|
1045
|
+
import { defineConfig } from 'vitest/config';
|
|
1046
|
+
|
|
1047
|
+
export default defineConfig({
|
|
1048
|
+
test: {
|
|
1049
|
+
globals: true,
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### Testing Routes
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
import { describe, it, expect } from 'vitest';
|
|
1058
|
+
import { createTestContext, createMockLogger } from '@blaizejs/testing-utils';
|
|
1059
|
+
import { GET } from './routes/users/[userId]';
|
|
625
1060
|
|
|
626
|
-
|
|
627
|
-
|
|
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
|
+
```
|
|
628
1088
|
|
|
629
|
-
|
|
630
|
-
pnpm test
|
|
1089
|
+
### Testing Middleware
|
|
631
1090
|
|
|
632
|
-
|
|
633
|
-
|
|
1091
|
+
```typescript
|
|
1092
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
1093
|
+
import { createTestContext } from '@blaizejs/testing-utils';
|
|
1094
|
+
import { authMiddleware } from './middleware/auth';
|
|
634
1095
|
|
|
635
|
-
|
|
636
|
-
|
|
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
|
+
});
|
|
637
1121
|
```
|
|
638
1122
|
|
|
639
|
-
###
|
|
1123
|
+
### Mocking Services
|
|
640
1124
|
|
|
1125
|
+
```typescript
|
|
1126
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
1127
|
+
import { createTestContext } from '@blaizejs/testing-utils';
|
|
1128
|
+
import { POST } from './routes/users';
|
|
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
|
+
});
|
|
641
1157
|
```
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1158
|
+
|
|
1159
|
+
### Mock Logger
|
|
1160
|
+
|
|
1161
|
+
```typescript
|
|
1162
|
+
import { createMockLogger } from '@blaizejs/testing-utils';
|
|
1163
|
+
|
|
1164
|
+
const logger = createMockLogger();
|
|
1165
|
+
|
|
1166
|
+
// Use in tests
|
|
1167
|
+
await handler(ctx, params, logger);
|
|
1168
|
+
|
|
1169
|
+
// Assert logs
|
|
1170
|
+
expect(logger.logs).toContainEqual({
|
|
1171
|
+
level: 'info',
|
|
1172
|
+
message: 'User created',
|
|
1173
|
+
meta: expect.objectContaining({ userId: '123' }),
|
|
1174
|
+
});
|
|
656
1175
|
```
|
|
657
1176
|
|
|
658
|
-
|
|
1177
|
+
See [`@blaizejs/testing-utils`](../blaize-testing-utils/README.md) for the full testing API.
|
|
1178
|
+
|
|
1179
|
+
---
|
|
1180
|
+
|
|
1181
|
+
## 🗺️ Roadmap
|
|
1182
|
+
|
|
1183
|
+
### 🎯 v1.0 (Stable)
|
|
1184
|
+
|
|
1185
|
+
- [ ] Redis adapter for queue plugin
|
|
1186
|
+
- [ ] Rate limiting plugin (`@blaizejs/plugin-rate-limit`)
|
|
1187
|
+
- [ ] Compression middleware (`@blaizejs/middleware-compression`)
|
|
1188
|
+
- [ ] Database plugin with migrations (`@blaizejs/plugin-db`)
|
|
1189
|
+
- [ ] Storage plugin (`@blaizejs/plugin-storage`)
|
|
1190
|
+
- [ ] OpenAPI/Swagger generation
|
|
659
1191
|
|
|
660
|
-
|
|
1192
|
+
### 🔮 Future
|
|
661
1193
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1194
|
+
- [ ] Authentication plugin (`@blaizejs/plugin-auth`)
|
|
1195
|
+
- [ ] Edge runtime support
|
|
1196
|
+
- [ ] External queue workers
|
|
1197
|
+
- [ ] HTTP/2 hosting solutions
|
|
1198
|
+
- [ ] Deeper AI integrations
|
|
667
1199
|
|
|
668
1200
|
---
|
|
669
1201
|
|
|
670
|
-
|
|
1202
|
+
## 📚 Related
|
|
1203
|
+
|
|
1204
|
+
- [`@blaizejs/client`](../blaize-client/README.md) — Type-safe RPC client
|
|
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
|
|
1208
|
+
|
|
1209
|
+
---
|
|
1210
|
+
|
|
1211
|
+
## 📄 License
|
|
1212
|
+
|
|
1213
|
+
MIT © [BlaizeJS Contributors](https://github.com/jleajones/blaize/graphs/contributors)
|
|
1214
|
+
|
|
1215
|
+
---
|
|
671
1216
|
|
|
672
|
-
|
|
1217
|
+
**Built with ❤️ by the BlaizeJS team**
|