Drop a file in src/api/ and it's a route. Every CPU core serves traffic by default. Heavy work gets its own queue. One ignite() call. No ceremony.
$ npx create-efc-app my-api $ cd my-api && efc start dev ✔ Worker 1 ready on :3000 ✔ Worker 2 ready on :3000 ✔ Worker 3 ready on :3000 ✔ Worker 4 ready on :3000 → GET /health src/api/health.ts → GET /users src/api/users/index.ts → POST /users src/api/users/index.ts → GET /users/:id src/api/users/[id].ts
Most Express apps end up with the same three pain points. EFC solves all three at the convention level.
src/api/users/[id].ts becomes /users/:id. No router files, no app.use() chains. Add a file, get a route. Rename it, the URL renames too.
Node runs on one core by default. EFC forks one worker per CPU at startup, lets the OS distribute connections, and respawns crashed workers before you notice.
Sending an email or resizing an image shouldn't block a response. Files in src/tasks/ run off the request path — in a queue, with retries, optionally in their own thread.
You choose http-only cookies or Bearer tokens once, during create-efc-app. A real JWT_SECRET lands in .env automatically. export const middlewares = [requireAuth] is all protection takes.
defineModel gives you identical .find(), .create(), .delete() calls whether you're on MongoDB or PostgreSQL. The engine is an implementation detail.
efc doctor ships with itRun efc doctor to check config, env vars, DB connectivity, and worker setup before you deploy. efc routes prints the resolved route table if something looks wrong.
EFC walks src/api/ at boot and registers every file as a route. [id].ts → :id. index.ts → the directory path. Unregistered HTTP verbs get a 405 back — you don't write that logic.
export const middlewares = [...]compose()import type { Request, Response } from 'express'; import { User } from '../../models/User'; import { HttpError } from 'express-file-cluster'; export const GET = async (req: Request, res: Response) => { const user = await User.findById(req.params.id); if (!user) throw new HttpError(404, 'User not found'); res.json(user); }; export const DELETE = async (req: Request, res: Response) => { await User.delete(req.params.id); res.status(204).send(); };
A file in src/tasks/ is a named job. Call enqueue('SendEmail', payload) from any handler and return immediately. EFC picks the right execution model — event loop for I/O, worker_threads for CPU.
schedule in defineTaskimport { defineTask } from 'express-file-cluster/tasks'; // Task name = filename export default defineTask<{ to: string; subject: string }>( async (payload) => { await mailer.send(payload); } ); // Trigger from a route — respond immediately export const POST = async (req, res) => { const user = await User.create(req.body); await enqueue('SendEmail', { to: user.email }); res.status(202).json({ id: user.id, queued: true }); };
Answer one question during scaffolding: SSR app or SPA? EFC writes the login/logout route stubs, sets the cookie flags, generates a real JWT_SECRET, and puts requireAuth in scope. You protect a route with a single export.
.envimport { issueToken, requireAuth } from 'express-file-cluster/auth'; export const POST = async (req, res) => { const user = await verifyCredentials(req.body); issueToken(res, { sub: user.id, role: user.role }); res.json({ message: 'Logged in' }); }; // src/api/users/index.ts — protect with one line export const middlewares = [requireAuth]; export const GET = async (req, res) => { res.json(await User.find()); };
ignite() — one entry pointPass your dirs, your DB, your auth strategy. EFC runs the Pre-Flight sequence on every worker: connect → configure auth → scan routes → register tasks → listen. Each worker owns its own connection pool — nothing shared across cores.
os.cpus().lengthdev mode: single process, hot reload, source mapsimport { ignite } from 'express-file-cluster'; ignite({ port: 3000, apiDir: './src/api', tasksDir: './src/tasks', database: 'mongodb', databaseUrl: process.env.DATABASE_URL, authStrategy: 'http-only', jwtSecret: process.env.JWT_SECRET, cluster: true, tasks: { backend: 'bullmq', redisUrl: process.env.REDIS_URL, concurrency: 5, }, onWorkerReady: (id) => console.log(`Worker ${id} ready`), });
npx create-efc-appEvery routing rule, clustering decision, and task API is documented and designed. Phase 1 implementation starts now — if you've wanted to build a framework from scratch, this is the repo.