bxo 0.0.5-dev.8 → 0.0.5-dev.81
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 +281 -480
- package/example/cookie-example.ts +151 -0
- package/example/cors-example.ts +49 -0
- package/example/index.html +5 -0
- package/example/index.ts +191 -0
- package/example/multipart-example.ts +322 -0
- package/example/openapi-example.ts +132 -0
- package/example/passthrough-validation-example.ts +115 -0
- package/example/url-encoding-example.ts +93 -0
- package/example/websocket-example.ts +132 -0
- package/package.json +8 -8
- package/plugins/cors.ts +123 -73
- package/plugins/index.ts +2 -11
- package/plugins/openapi.ts +204 -0
- package/src/index.ts +989 -0
- package/test-url-encoding.ts +20 -0
- package/tsconfig.json +3 -5
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
- package/example.ts +0 -183
- package/index.ts +0 -835
- package/plugins/auth.ts +0 -119
- package/plugins/logger.ts +0 -109
- package/plugins/ratelimit.ts +0 -140
package/README.md
CHANGED
|
@@ -1,579 +1,380 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bxo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A fast, lightweight web framework for Bun with built-in Zod validation and lifecycle hooks.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- 🎮 **Server Management** - Programmatic start, stop, and restart capabilities
|
|
14
|
-
- 📊 **Status Monitoring** - Built-in server status and runtime statistics
|
|
15
|
-
- 📦 **Zero Dependencies** - Only depends on Zod for validation
|
|
16
|
-
- ⚡ **Fast** - Minimal overhead with efficient routing
|
|
7
|
+
- **Type-safe routing** with Zod schema validation
|
|
8
|
+
- **WebSocket support** with clean API
|
|
9
|
+
- **Lifecycle hooks** for middleware and plugins
|
|
10
|
+
- **Plugin system** for extending functionality
|
|
11
|
+
- **Built-in CORS support** via plugin
|
|
12
|
+
- **Fast performance** leveraging Bun's native HTTP server
|
|
17
13
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
### Installation
|
|
14
|
+
## Installation
|
|
21
15
|
|
|
22
16
|
```bash
|
|
23
|
-
bun
|
|
17
|
+
bun install
|
|
24
18
|
```
|
|
25
19
|
|
|
26
|
-
|
|
20
|
+
## Quick Start
|
|
27
21
|
|
|
28
22
|
```typescript
|
|
29
|
-
import BXO
|
|
30
|
-
|
|
31
|
-
const app = new BXO()
|
|
32
|
-
.get('/', async (ctx) => {
|
|
33
|
-
return { message: 'Hello, BXO!' };
|
|
34
|
-
})
|
|
35
|
-
.start(3000);
|
|
36
|
-
```
|
|
23
|
+
import BXO from "./src/index";
|
|
24
|
+
import { cors } from "./plugins";
|
|
37
25
|
|
|
38
|
-
|
|
26
|
+
const app = new BXO();
|
|
39
27
|
|
|
40
|
-
|
|
28
|
+
// Use CORS plugin
|
|
29
|
+
app.use(cors());
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
// Define routes
|
|
32
|
+
app.get("/", (ctx) => ctx.json({ message: "Hello World!" }));
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
const app = new BXO()
|
|
46
|
-
// Simple handler
|
|
47
|
-
.get('/simple', async (ctx) => {
|
|
48
|
-
return { message: 'Hello World' };
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
// With validation
|
|
52
|
-
.post('/users', async (ctx) => {
|
|
53
|
-
// ctx.body is fully typed
|
|
54
|
-
return { created: ctx.body };
|
|
55
|
-
}, {
|
|
56
|
-
body: z.object({
|
|
57
|
-
name: z.string(),
|
|
58
|
-
email: z.string().email()
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// Path parameters
|
|
63
|
-
.get('/users/:id', async (ctx) => {
|
|
64
|
-
// ctx.params.id is typed as UUID string
|
|
65
|
-
return { user: { id: ctx.params.id } };
|
|
66
|
-
}, {
|
|
67
|
-
params: z.object({
|
|
68
|
-
id: z.string().uuid()
|
|
69
|
-
}),
|
|
70
|
-
query: z.object({
|
|
71
|
-
include: z.string().optional()
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
// All HTTP methods supported
|
|
76
|
-
.put('/users/:id', handler)
|
|
77
|
-
.delete('/users/:id', handler)
|
|
78
|
-
.patch('/users/:id', handler);
|
|
34
|
+
app.start();
|
|
79
35
|
```
|
|
80
36
|
|
|
81
|
-
|
|
37
|
+
## WebSocket Support
|
|
82
38
|
|
|
83
|
-
|
|
39
|
+
BXO provides built-in WebSocket support with a clean, intuitive API:
|
|
84
40
|
|
|
85
41
|
```typescript
|
|
86
|
-
|
|
87
|
-
params: InferredParamsType; // Path parameters
|
|
88
|
-
query: InferredQueryType; // Query string parameters
|
|
89
|
-
body: InferredBodyType; // Request body
|
|
90
|
-
headers: InferredHeadersType; // Request headers
|
|
91
|
-
request: Request; // Original Request object
|
|
92
|
-
set: { // Response configuration
|
|
93
|
-
status?: number;
|
|
94
|
-
headers?: Record<string, string>;
|
|
95
|
-
};
|
|
96
|
-
user?: any; // Added by auth plugin
|
|
97
|
-
[key: string]: any; // Extended by plugins
|
|
98
|
-
}
|
|
99
|
-
```
|
|
42
|
+
import BXO from "./src";
|
|
100
43
|
|
|
101
|
-
|
|
44
|
+
const app = new BXO();
|
|
102
45
|
|
|
103
|
-
|
|
46
|
+
// WebSocket route
|
|
47
|
+
app.ws("/ws", {
|
|
48
|
+
open(ws) {
|
|
49
|
+
console.log("WebSocket connection opened");
|
|
50
|
+
ws.send("Welcome to BXO WebSocket!");
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
message(ws, message) {
|
|
54
|
+
console.log("Received message:", message);
|
|
55
|
+
// Echo the message back
|
|
56
|
+
ws.send(`Echo: ${message}`);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
close(ws, code, reason) {
|
|
60
|
+
console.log(`WebSocket connection closed: ${code} ${reason}`);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
ping(ws, data) {
|
|
64
|
+
console.log("Ping received:", data);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
pong(ws, data) {
|
|
68
|
+
console.log("Pong received:", data);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// WebSocket with path parameters
|
|
73
|
+
app.ws("/chat/:room", {
|
|
74
|
+
open(ws) {
|
|
75
|
+
const room = ws.data?.room || 'unknown';
|
|
76
|
+
console.log(`WebSocket connection opened for room: ${room}`);
|
|
77
|
+
ws.send(`Welcome to chat room: ${room}`);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
message(ws, message) {
|
|
81
|
+
const room = ws.data?.room || 'unknown';
|
|
82
|
+
console.log(`Message in room ${room}:`, message);
|
|
83
|
+
ws.send(`[${room}] Echo: ${message}`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
104
86
|
|
|
105
|
-
|
|
106
|
-
const config = {
|
|
107
|
-
params: z.object({ id: z.string().uuid() }),
|
|
108
|
-
query: z.object({
|
|
109
|
-
page: z.coerce.number().default(1),
|
|
110
|
-
limit: z.coerce.number().max(100).default(10)
|
|
111
|
-
}),
|
|
112
|
-
body: z.object({
|
|
113
|
-
name: z.string().min(1),
|
|
114
|
-
email: z.string().email()
|
|
115
|
-
}),
|
|
116
|
-
headers: z.object({
|
|
117
|
-
'content-type': z.literal('application/json')
|
|
118
|
-
})
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
app.post('/api/users/:id', async (ctx) => {
|
|
122
|
-
// All properties are fully typed based on schemas
|
|
123
|
-
const { id } = ctx.params; // string (UUID)
|
|
124
|
-
const { page, limit } = ctx.query; // number, number
|
|
125
|
-
const { name, email } = ctx.body; // string, string
|
|
126
|
-
}, config);
|
|
87
|
+
app.start();
|
|
127
88
|
```
|
|
128
89
|
|
|
129
|
-
|
|
90
|
+
### WebSocket Handler Events
|
|
130
91
|
|
|
131
|
-
|
|
92
|
+
- `open(ws)` - Called when a WebSocket connection is established
|
|
93
|
+
- `message(ws, message)` - Called when a message is received
|
|
94
|
+
- `close(ws, code, reason)` - Called when the connection is closed
|
|
95
|
+
- `drain(ws)` - Called when the WebSocket is ready for more data
|
|
96
|
+
- `ping(ws, data)` - Called when a ping is received
|
|
97
|
+
- `pong(ws, data)` - Called when a pong is received
|
|
132
98
|
|
|
133
|
-
###
|
|
99
|
+
### WebSocket Features
|
|
134
100
|
|
|
135
|
-
|
|
101
|
+
- **Path parameters** - Support for dynamic routes like `/chat/:room`
|
|
102
|
+
- **Automatic upgrade** - HTTP requests to WebSocket routes are automatically upgraded
|
|
103
|
+
- **Type safety** - Full TypeScript support with proper typing
|
|
104
|
+
- **Error handling** - Built-in error handling for WebSocket events
|
|
105
|
+
- **Data attachment** - Access to path information via `ws.data`
|
|
136
106
|
|
|
137
|
-
|
|
138
|
-
import { cors } from './plugins';
|
|
107
|
+
## Lifecycle Hooks
|
|
139
108
|
|
|
140
|
-
|
|
141
|
-
origin: ['http://localhost:3000', 'https://example.com'],
|
|
142
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
143
|
-
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
144
|
-
credentials: true,
|
|
145
|
-
maxAge: 86400
|
|
146
|
-
}));
|
|
147
|
-
```
|
|
109
|
+
BXO provides powerful lifecycle hooks that allow you to intercept and modify requests and responses at different stages:
|
|
148
110
|
|
|
149
|
-
|
|
111
|
+
### beforeRequest
|
|
112
|
+
Runs before any route processing. Can modify the request or return a response to short-circuit processing.
|
|
150
113
|
|
|
151
114
|
```typescript
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
includeBody: false, // Log request/response bodies
|
|
157
|
-
includeHeaders: false // Log headers
|
|
158
|
-
}));
|
|
115
|
+
app.beforeRequest(async (req) => {
|
|
116
|
+
console.log(`${req.method} ${req.url}`);
|
|
117
|
+
return req; // Continue with request
|
|
118
|
+
});
|
|
159
119
|
```
|
|
160
120
|
|
|
161
|
-
|
|
121
|
+
### afterRequest
|
|
122
|
+
Runs after route processing but before the response is sent. Can modify the final response.
|
|
162
123
|
|
|
163
124
|
```typescript
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
secret: 'your-secret-key',
|
|
169
|
-
exclude: ['/login', '/health'], // Skip auth for these paths
|
|
170
|
-
verify: async (token, ctx) => {
|
|
171
|
-
// Custom token verification
|
|
172
|
-
return { user: 'data' };
|
|
173
|
-
}
|
|
174
|
-
}));
|
|
175
|
-
|
|
176
|
-
// Create JWT tokens
|
|
177
|
-
const token = createJWT(
|
|
178
|
-
{ userId: 123, role: 'admin' },
|
|
179
|
-
'secret',
|
|
180
|
-
3600 // expires in 1 hour
|
|
181
|
-
);
|
|
125
|
+
app.afterRequest(async (req, res) => {
|
|
126
|
+
res.headers.set("X-Response-Time", Date.now().toString());
|
|
127
|
+
return res;
|
|
128
|
+
});
|
|
182
129
|
```
|
|
183
130
|
|
|
184
|
-
|
|
131
|
+
### beforeResponse
|
|
132
|
+
Runs after the route handler but before response headers are merged. Useful for modifying response metadata.
|
|
185
133
|
|
|
186
134
|
```typescript
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
window: 60, // Time window in seconds
|
|
192
|
-
exclude: ['/health'], // Skip rate limiting for these paths
|
|
193
|
-
keyGenerator: (ctx) => { // Custom key generation
|
|
194
|
-
return ctx.request.headers.get('x-api-key') || 'default';
|
|
195
|
-
},
|
|
196
|
-
message: 'Too many requests',
|
|
197
|
-
statusCode: 429
|
|
198
|
-
}));
|
|
135
|
+
app.beforeResponse(async (res) => {
|
|
136
|
+
res.headers.set("X-Custom-Header", "value");
|
|
137
|
+
return res;
|
|
138
|
+
});
|
|
199
139
|
```
|
|
200
140
|
|
|
201
|
-
###
|
|
141
|
+
### onError
|
|
142
|
+
Runs when an error occurs during request processing. Can return a custom error response.
|
|
202
143
|
|
|
203
144
|
```typescript
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
ctx.startTime = Date.now();
|
|
209
|
-
},
|
|
210
|
-
onResponse: async (ctx, response) => {
|
|
211
|
-
console.log(`Request took ${Date.now() - ctx.startTime}ms`);
|
|
212
|
-
return response;
|
|
213
|
-
},
|
|
214
|
-
onError: async (ctx, error) => {
|
|
215
|
-
console.error('Request failed:', error.message);
|
|
216
|
-
return { error: 'Custom error response' };
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
app.use(customPlugin);
|
|
145
|
+
app.onError(async (error, req) => {
|
|
146
|
+
console.error(`Error: ${error.message}`);
|
|
147
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
148
|
+
});
|
|
221
149
|
```
|
|
222
150
|
|
|
223
|
-
##
|
|
151
|
+
## Plugins
|
|
224
152
|
|
|
225
|
-
|
|
153
|
+
### CORS Plugin
|
|
226
154
|
|
|
227
|
-
|
|
155
|
+
The CORS plugin provides comprehensive Cross-Origin Resource Sharing support:
|
|
228
156
|
|
|
229
157
|
```typescript
|
|
230
|
-
|
|
231
|
-
.onBeforeStart(() => {
|
|
232
|
-
console.log('🔧 Preparing to start server...');
|
|
233
|
-
})
|
|
234
|
-
.onAfterStart(() => {
|
|
235
|
-
console.log('✅ Server fully started and ready!');
|
|
236
|
-
})
|
|
237
|
-
.onBeforeStop(() => {
|
|
238
|
-
console.log('🔧 Preparing to stop server...');
|
|
239
|
-
})
|
|
240
|
-
.onAfterStop(() => {
|
|
241
|
-
console.log('✅ Server fully stopped!');
|
|
242
|
-
})
|
|
243
|
-
.onBeforeRestart(() => {
|
|
244
|
-
console.log('🔧 Preparing to restart server...');
|
|
245
|
-
})
|
|
246
|
-
.onAfterRestart(() => {
|
|
247
|
-
console.log('✅ Server restart completed!');
|
|
248
|
-
});
|
|
249
|
-
```
|
|
158
|
+
import { cors } from "./plugins";
|
|
250
159
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
console.log(`📨 ${ctx.request.method} ${ctx.request.url}`);
|
|
257
|
-
})
|
|
258
|
-
.onResponse((ctx, response) => {
|
|
259
|
-
console.log(`📤 Response sent`);
|
|
260
|
-
return response; // Can modify response
|
|
261
|
-
})
|
|
262
|
-
.onError((ctx, error) => {
|
|
263
|
-
console.error(`💥 Error:`, error.message);
|
|
264
|
-
return { error: 'Something went wrong' }; // Can provide custom error response
|
|
265
|
-
});
|
|
160
|
+
app.use(cors({
|
|
161
|
+
origin: ["http://localhost:3000", "https://myapp.com"],
|
|
162
|
+
methods: ["GET", "POST", "PUT", "DELETE"],
|
|
163
|
+
credentials: true
|
|
164
|
+
}));
|
|
266
165
|
```
|
|
267
166
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
BXO includes built-in hot reload and comprehensive server management capabilities:
|
|
167
|
+
### OpenAPI Plugin
|
|
271
168
|
|
|
272
|
-
|
|
169
|
+
The OpenAPI plugin automatically generates OpenAPI 3.0 documentation with support for tags, security schemes, and comprehensive route metadata:
|
|
273
170
|
|
|
274
171
|
```typescript
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
172
|
+
import { openapi } from "./plugins";
|
|
173
|
+
|
|
174
|
+
app.use(openapi({
|
|
175
|
+
path: "/docs", // Swagger UI endpoint
|
|
176
|
+
jsonPath: "/openapi.json", // OpenAPI JSON endpoint
|
|
177
|
+
defaultTags: ["API"], // Default tags for routes
|
|
178
|
+
securitySchemes: { // Define security schemes
|
|
179
|
+
bearerAuth: {
|
|
180
|
+
type: "http",
|
|
181
|
+
scheme: "bearer",
|
|
182
|
+
bearerFormat: "JWT",
|
|
183
|
+
description: "JWT token for authentication"
|
|
184
|
+
},
|
|
185
|
+
apiKeyAuth: {
|
|
186
|
+
type: "apiKey",
|
|
187
|
+
in: "header",
|
|
188
|
+
name: "X-API-Key",
|
|
189
|
+
description: "API key for authentication"
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
globalSecurity: [ // Global security requirements
|
|
193
|
+
{ bearerAuth: [] },
|
|
194
|
+
{ apiKeyAuth: [] }
|
|
195
|
+
],
|
|
196
|
+
openapiConfig: { // Additional OpenAPI config
|
|
197
|
+
info: {
|
|
198
|
+
title: "My API",
|
|
199
|
+
version: "1.0.0",
|
|
200
|
+
description: "API description"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}));
|
|
283
204
|
```
|
|
284
205
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
```typescript
|
|
288
|
-
const app = new BXO();
|
|
289
|
-
|
|
290
|
-
// Start server
|
|
291
|
-
await app.start(3000, 'localhost');
|
|
292
|
-
|
|
293
|
-
// Check if server is running
|
|
294
|
-
if (app.isServerRunning()) {
|
|
295
|
-
console.log('Server is running!');
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Get server information
|
|
299
|
-
const info = app.getServerInfo();
|
|
300
|
-
console.log(info); // { running: true, hotReload: true, watchedFiles: ['./'] }
|
|
206
|
+
#### Route Metadata
|
|
301
207
|
|
|
302
|
-
|
|
303
|
-
await app.restart(3000, 'localhost');
|
|
304
|
-
|
|
305
|
-
// Stop server gracefully
|
|
306
|
-
await app.stop();
|
|
307
|
-
|
|
308
|
-
// Backward compatibility - listen() still works
|
|
309
|
-
await app.listen(3000); // Same as app.start(3000)
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### Development vs Production
|
|
208
|
+
Routes can include detailed metadata for better OpenAPI documentation:
|
|
313
209
|
|
|
314
210
|
```typescript
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
app.get('/dev/status', async (ctx) => {
|
|
330
|
-
return {
|
|
331
|
-
...app.getServerInfo(),
|
|
332
|
-
uptime: process.uptime(),
|
|
333
|
-
memory: process.memoryUsage()
|
|
334
|
-
};
|
|
335
|
-
});
|
|
336
|
-
}
|
|
211
|
+
app.get("/users/:id", (ctx) => {
|
|
212
|
+
const id = ctx.params.id
|
|
213
|
+
return { user: { id, name: "John Doe" } }
|
|
214
|
+
}, {
|
|
215
|
+
detail: {
|
|
216
|
+
tags: ["Users"], // Route tags for grouping
|
|
217
|
+
summary: "Get user by ID", // Operation summary
|
|
218
|
+
description: "Retrieve user details", // Detailed description
|
|
219
|
+
security: [{ bearerAuth: [] }], // Route-specific security
|
|
220
|
+
params: { // Parameter documentation
|
|
221
|
+
id: z.string().describe("User ID")
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
})
|
|
337
225
|
```
|
|
338
226
|
|
|
339
|
-
|
|
227
|
+
#### Supported Metadata Fields
|
|
340
228
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
229
|
+
- `tags`: Array of tags for grouping operations
|
|
230
|
+
- `summary`: Short description of the operation
|
|
231
|
+
- `description`: Detailed description of the operation
|
|
232
|
+
- `security`: Security requirements for the route
|
|
233
|
+
- `params`: Path parameter schemas and descriptions
|
|
234
|
+
- `query`: Query parameter schemas
|
|
235
|
+
- `hidden`: Set to `true` to exclude from OpenAPI docs
|
|
344
236
|
|
|
345
|
-
|
|
237
|
+
### Creating Custom Plugins
|
|
346
238
|
|
|
347
|
-
|
|
348
|
-
app.enableHotReload(['./']);
|
|
239
|
+
Plugins are just BXO instances with lifecycle hooks:
|
|
349
240
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
.use(cors({
|
|
354
|
-
origin: ['http://localhost:3000'],
|
|
355
|
-
credentials: true
|
|
356
|
-
}))
|
|
357
|
-
.use(rateLimit({
|
|
358
|
-
max: 100,
|
|
359
|
-
window: 60,
|
|
360
|
-
exclude: ['/health']
|
|
361
|
-
}))
|
|
362
|
-
.use(auth({
|
|
363
|
-
type: 'jwt',
|
|
364
|
-
secret: 'your-secret-key',
|
|
365
|
-
exclude: ['/', '/login', '/health']
|
|
366
|
-
}));
|
|
367
|
-
|
|
368
|
-
// Comprehensive lifecycle hooks
|
|
369
|
-
app
|
|
370
|
-
.onBeforeStart(() => console.log('🔧 Preparing server startup...'))
|
|
371
|
-
.onAfterStart(() => console.log('✅ Server ready!'))
|
|
372
|
-
.onBeforeRestart(() => console.log('🔄 Restarting server...'))
|
|
373
|
-
.onAfterRestart(() => console.log('✅ Server restarted!'))
|
|
374
|
-
.onError((ctx, error) => ({
|
|
375
|
-
error: 'Internal server error',
|
|
376
|
-
timestamp: new Date().toISOString()
|
|
377
|
-
}));
|
|
378
|
-
|
|
379
|
-
// Routes
|
|
380
|
-
app
|
|
381
|
-
.get('/health', async () => ({
|
|
382
|
-
status: 'ok',
|
|
383
|
-
timestamp: new Date().toISOString(),
|
|
384
|
-
server: app.getServerInfo()
|
|
385
|
-
}))
|
|
386
|
-
|
|
387
|
-
.post('/login', async (ctx) => {
|
|
388
|
-
const { username, password } = ctx.body;
|
|
389
|
-
|
|
390
|
-
if (username === 'admin' && password === 'password') {
|
|
391
|
-
const token = createJWT(
|
|
392
|
-
{ username, role: 'admin' },
|
|
393
|
-
'your-secret-key'
|
|
394
|
-
);
|
|
395
|
-
return { token, user: { username, role: 'admin' } };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
ctx.set.status = 401;
|
|
399
|
-
return { error: 'Invalid credentials' };
|
|
400
|
-
}, {
|
|
401
|
-
body: z.object({
|
|
402
|
-
username: z.string(),
|
|
403
|
-
password: z.string()
|
|
404
|
-
})
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
.get('/users/:id', async (ctx) => {
|
|
408
|
-
return {
|
|
409
|
-
user: {
|
|
410
|
-
id: ctx.params.id,
|
|
411
|
-
include: ctx.query.include
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
}, {
|
|
415
|
-
params: z.object({ id: z.string().uuid() }),
|
|
416
|
-
query: z.object({ include: z.string().optional() })
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
.post('/users', async (ctx) => {
|
|
420
|
-
return { created: ctx.body };
|
|
421
|
-
}, {
|
|
422
|
-
body: z.object({
|
|
423
|
-
name: z.string(),
|
|
424
|
-
email: z.string().email()
|
|
425
|
-
})
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
.get('/protected', async (ctx) => {
|
|
429
|
-
// ctx.user available from auth plugin
|
|
430
|
-
return {
|
|
431
|
-
message: 'Protected resource',
|
|
432
|
-
user: ctx.user
|
|
433
|
-
};
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
// Server management endpoints
|
|
437
|
-
.post('/restart', async (ctx) => {
|
|
438
|
-
setTimeout(() => app.restart(3000), 100);
|
|
439
|
-
return { message: 'Server restart initiated' };
|
|
440
|
-
})
|
|
241
|
+
```typescript
|
|
242
|
+
function loggingPlugin() {
|
|
243
|
+
const plugin = new BXO();
|
|
441
244
|
|
|
442
|
-
.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
uptime: process.uptime(),
|
|
446
|
-
memory: process.memoryUsage()
|
|
447
|
-
};
|
|
245
|
+
plugin.beforeRequest(async (req) => {
|
|
246
|
+
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
|
|
247
|
+
return req;
|
|
448
248
|
});
|
|
249
|
+
|
|
250
|
+
return plugin;
|
|
251
|
+
}
|
|
449
252
|
|
|
450
|
-
|
|
451
|
-
app.start(3000);
|
|
253
|
+
app.use(loggingPlugin());
|
|
452
254
|
```
|
|
453
255
|
|
|
454
|
-
##
|
|
455
|
-
|
|
456
|
-
With the example server running, test these endpoints:
|
|
457
|
-
|
|
458
|
-
```bash
|
|
459
|
-
# Health check
|
|
460
|
-
curl http://localhost:3000/health
|
|
256
|
+
## Route Validation
|
|
461
257
|
|
|
462
|
-
|
|
463
|
-
curl -X POST http://localhost:3000/login \
|
|
464
|
-
-H "Content-Type: application/json" \
|
|
465
|
-
-d '{"username": "admin", "password": "password"}'
|
|
258
|
+
Define Zod schemas for request validation:
|
|
466
259
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
260
|
+
```typescript
|
|
261
|
+
import { z } from "bxo";
|
|
262
|
+
|
|
263
|
+
const UserSchema = z.object({
|
|
264
|
+
name: z.string().min(1),
|
|
265
|
+
email: z.string().email()
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
app.post("/users", async (ctx) => {
|
|
269
|
+
const user = ctx.body; // Already validated by UserSchema
|
|
270
|
+
return ctx.json({ id: 1, ...user });
|
|
271
|
+
}, {
|
|
272
|
+
body: UserSchema
|
|
273
|
+
});
|
|
274
|
+
```
|
|
471
275
|
|
|
472
|
-
|
|
473
|
-
curl "http://localhost:3000/users/123e4567-e89b-12d3-a456-426614174000?include=profile"
|
|
276
|
+
## Multipart/Form-Data Parsing
|
|
474
277
|
|
|
475
|
-
|
|
476
|
-
curl http://localhost:3000/protected \
|
|
477
|
-
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
|
278
|
+
BXO automatically parses multipart/form-data into nested objects and arrays before Zod validation:
|
|
478
279
|
|
|
479
|
-
|
|
480
|
-
|
|
280
|
+
### Nested Objects
|
|
281
|
+
Form fields like `profile[name]` are automatically converted to nested objects:
|
|
481
282
|
|
|
482
|
-
|
|
483
|
-
|
|
283
|
+
```typescript
|
|
284
|
+
const UserFormSchema = z.object({
|
|
285
|
+
name: z.string(),
|
|
286
|
+
email: z.string().email(),
|
|
287
|
+
profile: z.object({
|
|
288
|
+
name: z.string()
|
|
289
|
+
})
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
app.post("/users", async (ctx) => {
|
|
293
|
+
// Form data: profile[name]="John" becomes { profile: { name: "John" } }
|
|
294
|
+
console.log(ctx.body); // { name: "...", email: "...", profile: { name: "John" } }
|
|
295
|
+
return ctx.json({ success: true, data: ctx.body });
|
|
296
|
+
}, {
|
|
297
|
+
body: UserFormSchema
|
|
298
|
+
});
|
|
484
299
|
```
|
|
485
300
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
### BXO Class Methods
|
|
489
|
-
|
|
490
|
-
#### HTTP Methods
|
|
491
|
-
- `get(path, handler, config?)` - Handle GET requests
|
|
492
|
-
- `post(path, handler, config?)` - Handle POST requests
|
|
493
|
-
- `put(path, handler, config?)` - Handle PUT requests
|
|
494
|
-
- `delete(path, handler, config?)` - Handle DELETE requests
|
|
495
|
-
- `patch(path, handler, config?)` - Handle PATCH requests
|
|
496
|
-
|
|
497
|
-
#### Plugins & Hooks
|
|
498
|
-
- `use(plugin)` - Add a plugin
|
|
499
|
-
- `onBeforeStart(handler)` - Before server start hook
|
|
500
|
-
- `onAfterStart(handler)` - After server start hook
|
|
501
|
-
- `onBeforeStop(handler)` - Before server stop hook
|
|
502
|
-
- `onAfterStop(handler)` - After server stop hook
|
|
503
|
-
- `onBeforeRestart(handler)` - Before server restart hook
|
|
504
|
-
- `onAfterRestart(handler)` - After server restart hook
|
|
505
|
-
- `onRequest(handler)` - Global request hook
|
|
506
|
-
- `onResponse(handler)` - Global response hook
|
|
507
|
-
- `onError(handler)` - Global error hook
|
|
508
|
-
|
|
509
|
-
#### Server Management
|
|
510
|
-
- `start(port?, hostname?)` - Start the server
|
|
511
|
-
- `stop()` - Stop the server gracefully
|
|
512
|
-
- `restart(port?, hostname?)` - Restart the server
|
|
513
|
-
- `listen(port?, hostname?)` - Start the server (backward compatibility)
|
|
514
|
-
- `isServerRunning()` - Check if server is running
|
|
515
|
-
- `getServerInfo()` - Get server status information
|
|
516
|
-
|
|
517
|
-
#### Hot Reload
|
|
518
|
-
- `enableHotReload(watchPaths?)` - Enable hot reload with file watching
|
|
519
|
-
|
|
520
|
-
### Route Configuration
|
|
301
|
+
### Arrays
|
|
302
|
+
Form fields like `items[0]`, `items[1]` are automatically converted to arrays:
|
|
521
303
|
|
|
522
304
|
```typescript
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
305
|
+
const ItemsSchema = z.object({
|
|
306
|
+
items: z.array(z.string()),
|
|
307
|
+
tags: z.array(z.string()),
|
|
308
|
+
profile: z.object({
|
|
309
|
+
name: z.string(),
|
|
310
|
+
age: z.string().transform(val => parseInt(val, 10))
|
|
311
|
+
})
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
app.post("/items", async (ctx) => {
|
|
315
|
+
// Form data: items[0]="Apple", items[1]="Banana" becomes { items: ["Apple", "Banana"] }
|
|
316
|
+
console.log(ctx.body); // { items: ["Apple", "Banana"], tags: [...], profile: {...} }
|
|
317
|
+
return ctx.json({ success: true, data: ctx.body });
|
|
318
|
+
}, {
|
|
319
|
+
body: ItemsSchema
|
|
320
|
+
});
|
|
529
321
|
```
|
|
530
322
|
|
|
531
|
-
###
|
|
323
|
+
### Deep Nested Array Objects
|
|
324
|
+
Form fields like `workspace_items[0][id]`, `workspace_items[0][type]` are automatically converted to arrays of objects:
|
|
532
325
|
|
|
533
326
|
```typescript
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
327
|
+
const WorkspaceSchema = z.object({
|
|
328
|
+
id: z.string(),
|
|
329
|
+
workspace_items: z.array(z.object({
|
|
330
|
+
id: z.string(),
|
|
331
|
+
type: z.string(),
|
|
332
|
+
value: z.string(),
|
|
333
|
+
options: z.string(),
|
|
334
|
+
label: z.string()
|
|
335
|
+
}))
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
app.post("/workspace", async (ctx) => {
|
|
339
|
+
// Form data: workspace_items[0][id]="item1", workspace_items[0][type]="Link"
|
|
340
|
+
// becomes { workspace_items: [{ id: "item1", type: "Link", ... }] }
|
|
341
|
+
console.log(ctx.body); // { id: "...", workspace_items: [{ id: "item1", type: "Link", ... }] }
|
|
342
|
+
return ctx.json({ success: true, data: ctx.body });
|
|
343
|
+
}, {
|
|
344
|
+
body: WorkspaceSchema
|
|
345
|
+
});
|
|
540
346
|
```
|
|
541
347
|
|
|
542
|
-
|
|
348
|
+
### Supported Patterns
|
|
349
|
+
- **Nested objects**: `profile[name]`, `settings[theme]` → `{ profile: { name: "..." }, settings: { theme: "..." } }`
|
|
350
|
+
- **Arrays**: `items[0]`, `items[1]` → `{ items: ["...", "..."] }`
|
|
351
|
+
- **Deep nested array objects**: `workspace_items[0][id]`, `workspace_items[0][type]` → `{ workspace_items: [{ id: "...", type: "..." }] }`
|
|
352
|
+
- **Duplicate keys**: Multiple values with same key → `{ tags: ["tag1", "tag2"] }`
|
|
543
353
|
|
|
544
|
-
|
|
354
|
+
## Running
|
|
545
355
|
|
|
546
356
|
```bash
|
|
547
|
-
|
|
548
|
-
bun run example.ts
|
|
549
|
-
|
|
550
|
-
# The server will automatically restart when you edit any .ts/.js files!
|
|
357
|
+
bun run ./src/index.ts
|
|
551
358
|
```
|
|
552
359
|
|
|
553
|
-
|
|
360
|
+
Or run the examples:
|
|
554
361
|
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
├── plugins/
|
|
559
|
-
│ ├── index.ts # Plugin exports
|
|
560
|
-
│ ├── cors.ts # CORS plugin
|
|
561
|
-
│ ├── logger.ts # Logger plugin
|
|
562
|
-
│ ├── auth.ts # Authentication plugin
|
|
563
|
-
│ └── ratelimit.ts # Rate limiting plugin
|
|
564
|
-
├── example.ts # Usage example
|
|
565
|
-
├── package.json
|
|
566
|
-
└── README.md
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
## 🤝 Contributing
|
|
362
|
+
```bash
|
|
363
|
+
# CORS example
|
|
364
|
+
bun run ./example/cors-example.ts
|
|
570
365
|
|
|
571
|
-
|
|
366
|
+
# Multipart form data parsing example
|
|
367
|
+
bun run ./example/multipart-example.ts
|
|
368
|
+
```
|
|
572
369
|
|
|
573
|
-
##
|
|
370
|
+
## Examples
|
|
574
371
|
|
|
575
|
-
|
|
372
|
+
Check out the `example/` directory for more usage examples:
|
|
576
373
|
|
|
577
|
-
|
|
374
|
+
- `cors-example.ts` - Demonstrates CORS plugin and lifecycle hooks
|
|
375
|
+
- `openapi-example.ts` - Demonstrates OpenAPI plugin with tags and security
|
|
376
|
+
- `websocket-example.ts` - Demonstrates WebSocket functionality with interactive HTML client
|
|
377
|
+
- `multipart-example.ts` - Demonstrates multipart/form-data parsing with nested objects and arrays
|
|
378
|
+
- `index.ts` - Basic routing example
|
|
578
379
|
|
|
579
|
-
|
|
380
|
+
This project was created using `bun init` in bun v1.2.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|