express-file-cluster 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +367 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,367 @@
1
+ # express-file-cluster  ·  `efc`
2
+
3
+ **File-based routing. Multi-core clustering. Background tasks. Zero boilerplate.**
4
+
5
+ EFC is an opinionated backend framework built on Express. Drop files in `src/api/` and they become routes. Every CPU core serves traffic automatically. Heavy work goes to a queue-backed task subsystem so requests stay fast.
6
+
7
+ > **Status: v0.2.1 (Beta).** The router, clustering, and auth are implemented. The MongoDB adapter and task queue backend are in active development.
8
+
9
+ ---
10
+
11
+ ## Why EFC
12
+
13
+ Most Express apps grow the same way: a working prototype, then a maze of `router.get(...)` calls spread across files, a clustering setup copy-pasted from a blog post, and background jobs bolted on as an afterthought. EFC collapses all of that into conventions:
14
+
15
+ | Problem | EFC's answer |
16
+ |---|---|
17
+ | Route registration ceremony | The file tree **is** the route tree |
18
+ | Single-threaded Node under load | Auto-detected CPU count → worker processes |
19
+ | Blocking work on the request path | `enqueue()` ships it to a queue; respond immediately |
20
+ | Wiring auth, DB, and middleware by hand | `ignite()` — one call bootstraps everything |
21
+
22
+ ---
23
+
24
+ ## Quick Start
25
+
26
+ ```bash
27
+ npx create-efc-app my-api
28
+ cd my-api
29
+ efc start dev
30
+ ```
31
+
32
+ The interactive scaffolder asks for language, database, auth strategy, and whether you want clustering and background tasks — then writes the boilerplate, generates a `.env` with a real `JWT_SECRET`, and runs `npm install`.
33
+
34
+ ---
35
+
36
+ ## Project Structure
37
+
38
+ ```
39
+ my-api/
40
+ ├── src/
41
+ │ ├── api/ # Every file here is a route
42
+ │ │ ├── health.ts # GET /health
43
+ │ │ ├── users/
44
+ │ │ │ ├── index.ts # GET /users • POST /users
45
+ │ │ │ └── [id].ts # GET /users/:id • DELETE /users/:id
46
+ │ │ └── posts/
47
+ │ │ └── [slug]/
48
+ │ │ └── comments.ts # GET /posts/:slug/comments
49
+ │ ├── tasks/ # Background jobs
50
+ │ │ ├── SendEmail.ts
51
+ │ │ └── ResizeImage.ts
52
+ │ ├── models/ # Engine-agnostic models
53
+ │ │ └── User.ts
54
+ │ └── index.ts # Framework entry point
55
+ ├── efc.config.ts
56
+ ├── .env # Gitignored — JWT_SECRET auto-filled
57
+ └── .env.example
58
+ ```
59
+
60
+ Routing rules:
61
+
62
+ | File | URL |
63
+ |---|---|
64
+ | `api/health.ts` | `/health` |
65
+ | `api/users/index.ts` | `/users` |
66
+ | `api/users/[id].ts` | `/users/:id` |
67
+ | `api/posts/[slug]/comments.ts` | `/posts/:slug/comments` |
68
+
69
+ ---
70
+
71
+ ## Route Handlers
72
+
73
+ Export uppercase HTTP method names. Anything not exported returns **405 Method Not Allowed** automatically.
74
+
75
+ ```ts
76
+ // src/api/users/index.ts
77
+ import type { Request, Response } from 'express';
78
+ import { User } from '../../models/User';
79
+
80
+ export const GET = async (req: Request, res: Response) => {
81
+ const users = await User.find();
82
+ res.json(users);
83
+ };
84
+
85
+ export const POST = async (req: Request, res: Response) => {
86
+ const user = await User.create(req.body);
87
+ res.status(201).json({ id: user.id });
88
+ };
89
+ ```
90
+
91
+ ```ts
92
+ // src/api/users/[id].ts
93
+ import type { Request, Response } from 'express';
94
+ import { User } from '../../models/User';
95
+ import { HttpError } from 'express-file-cluster';
96
+
97
+ export const GET = async (req: Request, res: Response) => {
98
+ const user = await User.findById(req.params.id);
99
+ if (!user) throw new HttpError(404, 'User not found');
100
+ res.json(user);
101
+ };
102
+
103
+ export const DELETE = async (req: Request, res: Response) => {
104
+ await User.delete(req.params.id);
105
+ res.status(204).send();
106
+ };
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Middleware
112
+
113
+ Three tiers, each with a clear scope:
114
+
115
+ ```ts
116
+ // 1. Global — applies to every request
117
+ // CORS is built-in — configure it via CORS_ORIGINS in .env, not a separate package
118
+ ignite({ globalMiddlewares: [rateLimiter()] });
119
+
120
+ // 2. Route-level — applies to every handler in this file
121
+ export const middlewares = [requireAuth];
122
+
123
+ // 3. Handler-level — applies to one handler via compose()
124
+ import { compose } from 'express-file-cluster';
125
+
126
+ export const POST = compose(
127
+ validateBody(CreateUserSchema),
128
+ async (req, res) => { /* req.body is validated */ },
129
+ );
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Background Tasks
135
+
136
+ Tasks run off the request path — respond immediately, let the queue handle the work.
137
+
138
+ ```ts
139
+ // src/tasks/SendEmail.ts
140
+ import { defineTask } from 'express-file-cluster/tasks';
141
+
142
+ interface Payload { to: string; subject: string; body: string }
143
+
144
+ export default defineTask<Payload>(async (payload) => {
145
+ await mailer.send(payload);
146
+ });
147
+ ```
148
+
149
+ ```ts
150
+ // src/tasks/ResizeImage.ts — CPU-bound: runs in a worker_threads thread
151
+ import { defineTask } from 'express-file-cluster/tasks';
152
+
153
+ export default defineTask<{ key: string; width: number }>(
154
+ { thread: true },
155
+ async ({ key, width }) => {
156
+ const buf = await sharp(await download(key)).resize(width).toBuffer();
157
+ await upload(`${key}@${width}`, buf);
158
+ },
159
+ );
160
+ ```
161
+
162
+ ```ts
163
+ // Trigger from a route handler
164
+ import { enqueue } from 'express-file-cluster/tasks';
165
+
166
+ export const POST = async (req, res) => {
167
+ const user = await User.create(req.body);
168
+ await enqueue('SendEmail', { to: user.email, subject: 'Welcome!', body: '...' });
169
+ res.status(202).json({ id: user.id, queued: true });
170
+ };
171
+ ```
172
+
173
+ Task options:
174
+
175
+ | Option | Default | Description |
176
+ |---|---|---|
177
+ | `thread` | `false` | Run in a `worker_threads` thread (CPU-bound work) |
178
+ | `retries` | `3` | Retry attempts before dead-lettering |
179
+ | `backoff` | `'exponential'` | Delay strategy between retries |
180
+ | `concurrency` | `tasks.concurrency` | Parallel jobs for this task |
181
+ | `schedule` | — | Cron expression for recurring tasks |
182
+
183
+ ---
184
+
185
+ ## Authentication
186
+
187
+ ### `http-only` (recommended for SSR/SSG)
188
+
189
+ Tokens stored in `HttpOnly + Secure + SameSite=Strict` cookies.
190
+
191
+ ```ts
192
+ import { issueToken, revokeToken, requireAuth } from 'express-file-cluster/auth';
193
+
194
+ // src/api/auth/login.ts
195
+ export const POST = async (req, res) => {
196
+ const user = await verifyCredentials(req.body);
197
+ issueToken(res, { sub: user.id, role: user.role });
198
+ res.json({ message: 'Logged in' });
199
+ };
200
+
201
+ // Protect a route
202
+ export const middlewares = [requireAuth];
203
+ ```
204
+
205
+ ### `localStorage` (SPA-friendly)
206
+
207
+ Token returned in body; client attaches `Authorization: Bearer <token>`.
208
+
209
+ ```ts
210
+ import { signToken } from 'express-file-cluster/auth';
211
+
212
+ export const POST = async (req, res) => {
213
+ const token = signToken({ sub: user.id });
214
+ res.json({ token });
215
+ };
216
+ ```
217
+
218
+ ---
219
+
220
+ ## Bootstrapper
221
+
222
+ ```ts
223
+ // src/index.ts
224
+ import { ignite } from 'express-file-cluster';
225
+ import path from 'path';
226
+ import { fileURLToPath } from 'url';
227
+
228
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
229
+
230
+ ignite({
231
+ port: Number(process.env.PORT) || 3000,
232
+ apiDir: path.join(__dirname, 'api'),
233
+ tasksDir: path.join(__dirname, 'tasks'),
234
+
235
+ database: 'mongodb',
236
+ databaseUrl: process.env.DATABASE_URL,
237
+
238
+ authStrategy: 'http-only',
239
+ jwtSecret: process.env.JWT_SECRET,
240
+
241
+ cluster: true, // false → single process (auto-disabled in dev)
242
+ workers: 4, // defaults to os.cpus().length
243
+
244
+ tasks: {
245
+ backend: 'bullmq',
246
+ redisUrl: process.env.REDIS_URL,
247
+ concurrency: 5,
248
+ },
249
+
250
+ globalMiddlewares: [],
251
+ onWorkerReady: (id) => console.log(`Worker ${id} ready`),
252
+ onWorkerCrash: (id, code) => console.error(`Worker ${id} crashed (${code})`),
253
+ });
254
+ ```
255
+
256
+ ### `ignite()` options
257
+
258
+ | Option | Type | Default | Description |
259
+ |---|---|---|---|
260
+ | `port` | `number` | `3000` | HTTP listen port |
261
+ | `apiDir` | `string` | — | Path to route modules |
262
+ | `tasksDir` | `string` | — | Path to task modules |
263
+ | `database` | `'mongodb' \| 'postgresql'` | — | Database engine |
264
+ | `databaseUrl` | `string` | `DATABASE_URL` | Connection string |
265
+ | `authStrategy` | `'http-only' \| 'localStorage'` | — | Token delivery |
266
+ | `jwtSecret` | `string` | `JWT_SECRET` | JWT signing secret |
267
+ | `cluster` | `boolean` | `true` | Enable multi-core clustering |
268
+ | `workers` | `number` | `os.cpus().length` | Worker count override |
269
+ | `tasks` | `TaskConfig \| false` | `false` | Background task runtime |
270
+ | `cors` | `boolean \| CorsConfig` | `true` | CORS — origins driven by `CORS_ORIGINS` env var |
271
+ | `globalMiddlewares` | `RequestHandler[]` | `[]` | Applied to every route |
272
+ | `onWorkerReady` | `(id) => void` | — | Called when a worker boots |
273
+ | `onWorkerCrash` | `(id, code) => void` | — | Called before respawn |
274
+ | `onError` | `ErrorRequestHandler` | built-in | Override global error handler |
275
+
276
+ ---
277
+
278
+ ## CLI Reference
279
+
280
+ ```bash
281
+ # Development
282
+ efc start dev # Hot-reload single process, source maps, pretty logs
283
+
284
+ # Production
285
+ efc build prod # Type-check + compile to dist/ (tsup, dual CJS/ESM)
286
+ efc start prod # Run dist/ with clustering enabled
287
+
288
+ # Tests
289
+ efc run tests # Vitest (--watch, --coverage passthrough)
290
+
291
+ # Code generation
292
+ efc generate route users/[id] # → src/api/users/[id].ts
293
+ efc generate task ProcessPayment # → src/tasks/ProcessPayment.ts
294
+ efc generate middleware authorize # → src/middlewares/authorize.ts
295
+
296
+ # Diagnostics
297
+ efc routes # Print resolved route table (path → file → methods)
298
+ efc tasks # List registered background tasks
299
+ efc doctor # Validate config, env vars, DB connectivity
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Clustering Architecture
305
+
306
+ ```
307
+ Master Process
308
+ ┌────────────────────┐
309
+ │ fork × N workers │
310
+ │ respawn on crash │
311
+ └──┬──────┬──────┬───┘
312
+ │ │ │
313
+ Worker 1 Worker 2 Worker N
314
+ Pre-Flight lifecycle per worker:
315
+ 1. Connect DB
316
+ 2. Configure auth
317
+ 3. Scan apiDir → route map
318
+ 4. Register tasks
319
+ 5. Mount routes on Express
320
+ 6. Listen (OS round-robins connections)
321
+ ```
322
+
323
+ CPU-bound tasks fan out further into `worker_threads` — the request loop stays unblocked at every layer.
324
+
325
+ ---
326
+
327
+ ## Error Handling
328
+
329
+ ```ts
330
+ import { HttpError } from 'express-file-cluster';
331
+
332
+ // Throw from any handler — caught and formatted automatically
333
+ export const GET = async (req, res) => {
334
+ const user = await User.findById(req.params.id);
335
+ if (!user) throw new HttpError(404, 'User not found');
336
+ res.json(user);
337
+ };
338
+
339
+ // Override the global handler
340
+ ignite({
341
+ onError: (err, req, res, next) => {
342
+ logger.error(err);
343
+ res.status(err.statusCode ?? 500).json({ error: err.message });
344
+ },
345
+ });
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Environment Variables
351
+
352
+ `create-efc-app` generates `.env` (gitignored, `JWT_SECRET` pre-filled) and `.env.example` (committed, documented).
353
+
354
+ | Variable | Required | Description |
355
+ |---|---|---|
356
+ | `PORT` | No (default `3000`) | HTTP listen port |
357
+ | `NODE_ENV` | No | `development \| production \| test` |
358
+ | `DATABASE_URL` | Yes | MongoDB or PostgreSQL connection string |
359
+ | `JWT_SECRET` | Yes | JWT signing key — auto-generated by scaffolder |
360
+ | `JWT_EXPIRES_IN` | No (default `7d`) | Token lifetime |
361
+ | `COOKIE_DOMAIN` | No | Cookie domain for `http-only` auth |
362
+ | `REDIS_URL` | If using BullMQ | Redis connection for the task queue |
363
+ | `CORS_ORIGINS` | No | Comma-separated allowed origins — e.g. `http://localhost:3000,https://myapp.com` |
364
+
365
+ ## License
366
+
367
+ MIT © 2026 EFC Contributors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-file-cluster",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Enterprise-grade, zero-boilerplate backend framework with file-based routing and multi-core clustering",
5
5
  "license": "MIT",
6
6
  "type": "module",