Pre-alpha · Architecture locked · Code in progress

Express, but your folder
is your router.

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.

terminal
$ 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

Three things Express
never gave you out of the box.

Most Express apps end up with the same three pain points. EFC solves all three at the convention level.

Routing by filesystem

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.

CPU-aware from day one

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.

Tasks aren't routes

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.

Auth picked at scaffold time

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.

Same model, any database

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 it

Run 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.

Less setup.
More shipping.

Your folder structure is your API

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.

  • Rename a file → rename the route
  • Per-file middleware: export const middlewares = [...]
  • Per-handler guards via compose()
src/api/users/[id].ts
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();
};

202 now, do the work later

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.

  • BullMQ (Redis) or pg-boss — you pick
  • Automatic retries + exponential backoff
  • Cron scheduling: set schedule in defineTask
src/tasks/SendEmail.ts
import { 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 });
};

Pick once, forget about it

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.

  • HttpOnly + Secure + SameSite=Strict out of the box
  • No secret management ceremony — it's in .env
  • Role payload baked into the token, readable in any handler
src/api/auth/login.ts
import { 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 point

Pass 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.

  • Worker count defaults to os.cpus().length
  • Crashed workers are replaced before the next request lands
  • dev mode: single process, hot reload, source maps
src/index.ts
import { 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`),
});

Read the spec.
Then build.

Architecture done.
Code next.

Phase 0
Now

Design & Planning

  • API surface design
  • Architecture documentation
  • Monorepo scaffold
Phase 1
Q3 2026

Core MVP

  • File-based router
  • Multi-core clustering
  • Auth + MongoDB adapter
  • Background tasks (BullMQ)
  • npx create-efc-app
Phase 2
Q4 2026

Beta

  • PostgreSQL adapter
  • Zod validation integration
  • Structured logging
  • Cron task scheduling
Phase 3
Q1 2027

Stable v1.0

  • Plugin / adapter API
  • WebSocket support
  • OpenAPI spec generation
  • OpenTelemetry tracing

The spec is written.
The code needs you.

Every 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.