bro-framework 2.4.0 → 2.4.2
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 +112 -3
- package/bin/bro.js +20 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,8 +27,11 @@
|
|
|
27
27
|
## Table of Contents
|
|
28
28
|
|
|
29
29
|
- [Getting Started](#getting-started)
|
|
30
|
+
- [Configuration](#configuration)
|
|
30
31
|
- [The Core Experience](#the-core-experience)
|
|
31
32
|
- [Deep-Dive Features](#deep-dive-features)
|
|
33
|
+
- [Route Reference](#route-reference)
|
|
34
|
+
- [Context Reference](#context-reference)
|
|
32
35
|
- [Architecture & Request Lifecycle](#architecture--request-lifecycle)
|
|
33
36
|
- [Tech Stack Breakdown](#tech-stack-breakdown)
|
|
34
37
|
- [CLI Reference](#cli-reference)
|
|
@@ -48,6 +51,38 @@ npm run dev
|
|
|
48
51
|
|
|
49
52
|
That's it! Your zero-boilerplate backend is now running with hot-reloading enabled.
|
|
50
53
|
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
Configuration lives in `bro.config.js`:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
import { defineConfig } from 'bro-framework';
|
|
60
|
+
|
|
61
|
+
export default defineConfig({
|
|
62
|
+
server: { port: 5000, cors: true, helmet: true },
|
|
63
|
+
auth: {
|
|
64
|
+
jwtSecret: process.env.JWT_SECRET,
|
|
65
|
+
expiresIn: '7d',
|
|
66
|
+
apiKey: process.env.API_KEY
|
|
67
|
+
},
|
|
68
|
+
locale: { directory: './locale', defaultLocale: 'en' },
|
|
69
|
+
rateLimit: { windowMs: 15 * 60 * 1000, max: 100 },
|
|
70
|
+
docs: process.env.NODE_ENV !== 'production',
|
|
71
|
+
redisUrl: process.env.REDIS_URL,
|
|
72
|
+
db: async () => {
|
|
73
|
+
// Initialize database connection here
|
|
74
|
+
},
|
|
75
|
+
sockets: async (io, db) => {
|
|
76
|
+
// Setup Socket.IO event listeners here
|
|
77
|
+
},
|
|
78
|
+
onShutdown: async (db) => {
|
|
79
|
+
// Close application-owned database resources here.
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`cors: false` disables HTTP CORS middleware. Helmet is enabled by default and can be disabled with `helmet: false`. Redis is optional; when configured it powers distributed rate limiting, route caching, and Socket.IO scaling. In `NODE_ENV=test`, bro.js can inject `ioredis-mock`; install it in the consuming project's development dependencies.
|
|
85
|
+
|
|
51
86
|
---
|
|
52
87
|
|
|
53
88
|
## The Core Experience
|
|
@@ -68,6 +103,13 @@ export default defineRoute({
|
|
|
68
103
|
title: z.string().min(5),
|
|
69
104
|
content: z.string()
|
|
70
105
|
}),
|
|
106
|
+
query: z.object({
|
|
107
|
+
draft: z.coerce.boolean().default(false)
|
|
108
|
+
}),
|
|
109
|
+
response: z.object({
|
|
110
|
+
success: z.boolean(),
|
|
111
|
+
updated: z.number()
|
|
112
|
+
}),
|
|
71
113
|
handler: async ({ body, params, user, db, io }) => {
|
|
72
114
|
// 1. Data is already validated. user is already authenticated.
|
|
73
115
|
|
|
@@ -89,6 +131,8 @@ export default defineRoute({
|
|
|
89
131
|
});
|
|
90
132
|
```
|
|
91
133
|
|
|
134
|
+
Validation schemas are flat and must be declared directly as `body`, `params`, and `query`. The deprecated nested `schema: { body, params, query }` form is rejected during route loading. The optional `response` schema documents the successful JSON response in OpenAPI; it does not runtime-validate handler output.
|
|
135
|
+
|
|
92
136
|
---
|
|
93
137
|
|
|
94
138
|
## Deep-Dive Features
|
|
@@ -101,22 +145,47 @@ Powered by Zod. Attach a schema to `body`, `query`, or `params` directly in your
|
|
|
101
145
|
|
|
102
146
|
### Zero-Config Auth (JWTs, RBAC, API Keys)
|
|
103
147
|
Add `auth: true` to your route config. `bro.js` will intercept the request, extract the `Authorization: Bearer <token>` header, verify the signature using your `jwtSecret`, and inject the decoded payload directly into `ctx.user`.
|
|
104
|
-
You can also use Role-Based Access Control by passing an array of roles (e.g. `auth: ['admin']`) or enforce
|
|
148
|
+
You can also use Role-Based Access Control by passing an array of roles (e.g. `auth: ['admin']`) or enforce service-to-service communication with `auth: 'api-key'`. API keys are read from `auth.apiKey` or `API_KEY` and support zero-downtime rotation with an array:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
auth: {
|
|
152
|
+
apiKey: ['current-key', 'next-key']
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Clients send the selected key in the `x-api-key` header. API-key routes are represented as `apiKeyAuth` operations in OpenAPI.
|
|
105
157
|
|
|
106
158
|
### Context Injection
|
|
107
159
|
Stop importing singleton database connections and socket instances into every file. Define your `db` and `sockets` setup once in `bro.config.js`. `bro.js` orchestrates the initialization and injects both instances directly into the `ctx` object for every request handler.
|
|
108
160
|
|
|
161
|
+
### Redis Caching and Lifecycle
|
|
162
|
+
Set `redisUrl` to enable distributed rate limiting, route caching, and Socket.IO pub/sub scaling. Add `cache: 60` to a route to cache its JSON response for 60 seconds. Cache keys include the request URL, resolved locale, and authorization/API-key identity; do not cache responses with dimensions that are not represented in the key.
|
|
163
|
+
|
|
164
|
+
The programmatic `createServer()` API returns `shutdown()`. It stops scheduled tasks, closes Socket.IO, closes Redis clients, runs the optional `onShutdown(db)` hook, and closes the HTTP server. The CLI calls it automatically on `SIGINT` and `SIGTERM`.
|
|
165
|
+
|
|
109
166
|
### Zero-YAML Live Documentation
|
|
110
167
|
If you've ever hand-written OpenAPI YAML, you know the pain. `bro.js` parses your Zod schemas and automatically serves a stunning, interactive [Scalar](https://scalar.com/) API playground at `/docs`. It's highly secure: by default, these internal docs are disabled in production mode.
|
|
111
168
|
|
|
112
169
|
### The Frontend SDK Generator
|
|
113
|
-
Tired of writing frontend `fetch` wrappers? Run `bro sdk`. The CLI
|
|
170
|
+
Tired of writing frontend `fetch` wrappers? Run `bro sdk`. The CLI parses your backend routes and compiles a JavaScript `bro-sdk.js` file. It includes token and locale headers, query serialization, URL-encoded dynamic parameters, and deep tree traversal (e.g., `api.users.id("123").get()`). Configure it with `setBaseURL()`, `setTokenKey()`, and `setLocale()`.
|
|
114
171
|
|
|
115
172
|
### Background Task Scheduler
|
|
116
173
|
Don't spin up a separate worker server. Drop a JavaScript file anywhere in the `tasks/` folder, export a cron string (e.g., `"0 0 * * *"`), and an async handler. `bro.js` natively schedules it as a background worker with full access to your injected database and WebSocket contexts.
|
|
117
174
|
|
|
118
175
|
### Zero-Boilerplate File Uploads
|
|
119
|
-
Add `upload: true` to a route.
|
|
176
|
+
Add `upload: true` to a route. bro.js uses Multer to parse `multipart/form-data` and inject files into the context. Use `single`, `array`, or `fields` for explicit field handling:
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
export default defineRoute({
|
|
180
|
+
upload: {
|
|
181
|
+
single: 'avatar',
|
|
182
|
+
limits: { fileSize: 5 * 1024 * 1024 }
|
|
183
|
+
},
|
|
184
|
+
handler: ({ file }) => ({ name: file?.originalname })
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`ctx.file` is used by `single()`. `ctx.files` is an array for `array()` or a field-to-array object for `fields()`. Defaults include limits for file size, file count, fields, parts, and field size. Use `storage` for production disk/object-storage integration instead of retaining large buffers in memory.
|
|
120
189
|
|
|
121
190
|
### File-Based Locale
|
|
122
191
|
Create a `locale/` folder with one translation file per locale, such as `locale/en.js` and `locale/fr.js`. Export a plain object from each file, then use `t()` in any route:
|
|
@@ -133,6 +202,46 @@ export default defineRoute({
|
|
|
133
202
|
|
|
134
203
|
The locale is negotiated dynamically using RFC 9110 `Accept-Language` headers, supporting full region fallback and custom defaults, and the generated SDK can securely set it via `setLocale('fr')`.
|
|
135
204
|
|
|
205
|
+
## Route Reference
|
|
206
|
+
|
|
207
|
+
| Option | Type | Purpose |
|
|
208
|
+
| :--- | :--- | :--- |
|
|
209
|
+
| `auth` | `boolean \| string[] \| 'api-key'` | JWT, role, or API-key protection. |
|
|
210
|
+
| `body` | Zod schema | Validates JSON request bodies. |
|
|
211
|
+
| `params` | Zod schema | Validates URL parameters. |
|
|
212
|
+
| `query` | Zod schema | Validates query-string values. |
|
|
213
|
+
| `response` | Zod schema | Documents successful JSON output in OpenAPI. |
|
|
214
|
+
| `cache` | number | Redis response-cache duration in seconds. |
|
|
215
|
+
| `rateLimit` | `{ windowMs, max }` | Route-specific request limiting. |
|
|
216
|
+
| `upload` | boolean or options | Enables Multer parsing and limits. |
|
|
217
|
+
| `summary` | string | OpenAPI operation summary. |
|
|
218
|
+
|
|
219
|
+
Dynamic route files use bracket parameters such as `routes/users/[id].get.js`. Nested dynamic directories are supported. HTTP method suffixes are `get`, `post`, `put`, `delete`, `patch`, `options`, and `head`.
|
|
220
|
+
|
|
221
|
+
## Context Reference
|
|
222
|
+
|
|
223
|
+
Handlers receive:
|
|
224
|
+
|
|
225
|
+
| Property | Description |
|
|
226
|
+
| :--- | :--- |
|
|
227
|
+
| `body` | Validated body data. |
|
|
228
|
+
| `params` | Validated path parameters. |
|
|
229
|
+
| `query` | Validated query data. |
|
|
230
|
+
| `user` | Decoded JWT payload when JWT auth is used. |
|
|
231
|
+
| `db` | Value returned by `config.db`. |
|
|
232
|
+
| `io` | Socket.IO server instance. |
|
|
233
|
+
| `redis` | Redis client when Redis is enabled or test mode is active. |
|
|
234
|
+
| `file` | Single uploaded file from `upload.single()`. |
|
|
235
|
+
| `files` | Array or field map from `array()`/`fields()`. |
|
|
236
|
+
| `locale` | Resolved request locale. |
|
|
237
|
+
| `t` | Translation function, `t(key, values)`. |
|
|
238
|
+
| `jwt` | Configured JWT signing helper. |
|
|
239
|
+
| `env` | Parsed environment data when configured, otherwise `process.env`. |
|
|
240
|
+
|
|
241
|
+
## Testing
|
|
242
|
+
|
|
243
|
+
For Redis-backed integration tests without an external Redis server, install `ioredis-mock` in the consuming project and run with `NODE_ENV=test`. bro.js injects a mock Redis client and exercises cache, rate-limit, Socket.IO adapter, and shutdown paths.
|
|
244
|
+
|
|
136
245
|
---
|
|
137
246
|
|
|
138
247
|
## Architecture & Request Lifecycle
|
package/bin/bro.js
CHANGED
|
@@ -29,13 +29,15 @@ export default defineConfig({
|
|
|
29
29
|
// Server Settings
|
|
30
30
|
server: {
|
|
31
31
|
port: 5000,
|
|
32
|
-
cors: true // Set to true to allow all, or pass a CORS options object
|
|
32
|
+
cors: true, // Set to true to allow all, or pass a CORS options object
|
|
33
|
+
helmet: true // Enable security headers
|
|
33
34
|
},
|
|
34
35
|
|
|
35
36
|
// Authentication Settings
|
|
36
37
|
auth: {
|
|
37
38
|
jwtSecret: 'dev_secret_please_change',
|
|
38
|
-
expiresIn: '7d'
|
|
39
|
+
expiresIn: '7d',
|
|
40
|
+
apiKey: process.env.API_KEY || ['dev_key_1', 'dev_key_2'] // Supports array for zero-downtime rotation
|
|
39
41
|
},
|
|
40
42
|
|
|
41
43
|
// Optional file-based API translations
|
|
@@ -53,6 +55,9 @@ export default defineConfig({
|
|
|
53
55
|
max: 100 // limit each IP to 100 requests per windowMs
|
|
54
56
|
},
|
|
55
57
|
|
|
58
|
+
// Redis Configuration (Auto-scales WebSockets, distributed caches & rate-limiting)
|
|
59
|
+
redisUrl: process.env.REDIS_URL, // e.g., 'redis://localhost:6379'
|
|
60
|
+
|
|
56
61
|
// WebSockets Setup
|
|
57
62
|
sockets: async (io, db) => {
|
|
58
63
|
io.on('connection', (socket) => {
|
|
@@ -79,6 +84,11 @@ export default defineConfig({
|
|
|
79
84
|
// return mongoose.connection;
|
|
80
85
|
// --------------------------
|
|
81
86
|
return null;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Graceful Teardown Hook
|
|
90
|
+
onShutdown: async (db) => {
|
|
91
|
+
// Close application-owned database resources gracefully here
|
|
82
92
|
}
|
|
83
93
|
});
|
|
84
94
|
`;
|
|
@@ -264,6 +274,12 @@ async function bootstrap() {
|
|
|
264
274
|
|
|
265
275
|
if (command === 'dev' || command === 'start') {
|
|
266
276
|
bootstrap();
|
|
267
|
-
} else {
|
|
268
|
-
console.log(
|
|
277
|
+
} else if (!['sdk', 'generate-client', 'client', 'init'].includes(command)) {
|
|
278
|
+
console.log(`\n ${colors.bold}${colors.green}bro.js CLI${colors.reset}\n`);
|
|
279
|
+
console.log(` ${colors.bold}Usage:${colors.reset} bro <command>\n`);
|
|
280
|
+
console.log(` ${colors.bold}Commands:${colors.reset}`);
|
|
281
|
+
console.log(` ${colors.cyan}dev${colors.reset} Start the development server with hot-reload`);
|
|
282
|
+
console.log(` ${colors.cyan}start${colors.reset} Start the production server gracefully`);
|
|
283
|
+
console.log(` ${colors.cyan}init${colors.reset} Scaffold a new bro.config.js workspace`);
|
|
284
|
+
console.log(` ${colors.cyan}sdk${colors.reset} Generate a typed frontend client\n`);
|
|
269
285
|
}
|