0.9.0Migrations written from your models

Express, but your folder
is your router.

Drop a file in src/api/ and it's a route. Clustering, background jobs and a typed data layer are already set up.

$ npx create-efc-app my-api
Read the docs
The EFC development dashboard listing a project's routes, such as GET /v1/api/health, with method filters and a typed response example.
The EFC Workspace: a project tree with src/api, src/model and src/tasks, and src/index.ts calling ignite().

Left: the route dashboard every app serves in development. Right: the project that produced it.

Every Express app starts with the same chores: a router, a cluster, a queue, auth, a database layer. EFC does them once, so your app starts at the part that's actually yours.

Routing

The file tree is the route table.

EFC reads src/api/ when the server starts. Rename a file and its URL changes with it.

A file is a route

Export GET, POST and so on. Methods you leave out answer 405 by themselves.

Brackets are params

[id].ts becomes :id. Static paths always win, so /users/me never reaches /users/:id.

Optional and catch-all

[[id]] is optional. [...slug] takes the rest of the path as an array.

Validated on the way in

export const schema takes zod, valibot or arktype. Bad input gets a 400 before your handler runs.

src/api/health.ts
src/api/
health.ts
users/
index.ts
[id].ts
docs/
[...slug].ts
[[team]].ts
Routes
GET/health
GET/users
POST/users
GET/users/:id
GET/docs/:slug*
GET/:team?
// GET /health
export const GET = (req, res) => {
  res.json({ ok: true })
}

// POST /health → 405, no code needed
// GET /users/:id
import { HttpError } from 'express-file-cluster'

export const GET = async (req, res) => {
  const user = await User.findById(req.params.id)
  if (!user) throw new HttpError(404, 'Not found')
  res.json(user)
}
// GET /docs/guides/auth
export const GET = (req, res) => {
  req.params.slug  // ['guides', 'auth']
  res.json(lookup(req.params.slug))
}
import { z } from 'zod'

export const schema = {
  params: z.object({ id: z.string().uuid() }),
}

// GET /users/42 → 400 { issues: [...] }

Clustering

One worker per CPU core.

In production the primary process forks a worker for each core and replaces any that crash. Try crashing one.

On shutdown, SIGTERM reaches every worker, so open requests and running jobs finish first.

requests 0 · alive 4/4
primary :3000

Background jobs

Reply first. Do the slow work after.

Each file in src/tasks/ is a job. Queue it from a handler and return 202. It runs on BullMQ or pg-boss, with retries.

src/tasks/SendWelcome.ts
import { defineTask } from 'express-file-cluster/tasks'

export default defineTask<{ to: string }>(
  { retries: 3 },
  async ({ to }) => { await mailer.send(to) },
)

// in a route handler
await enqueue('SendWelcome', { to: user.email })
res.status(202).json({ queued: true })
POST /signup
Request received
validate, create user
0 ms
Job queued
enqueue('SendWelcome')
4 ms
202 sent
the user moves on
5 ms
Attempt 1 failed
SMTP timeout, retry in 1 s
2.1 s
Attempt 2 sent
job complete
3.4 s

Data

Describe the model. Get the types.

Types come from the schema, and tables or collections are created on first use. defineModel runs on MongoDB and PostgreSQL. defineTable adds relations on PostgreSQL.

src/model/Post.ts
export const Post = defineTable('Post', {
  title:  { type: 'string', required: true },
  author: { type: 'uuid', ref: 'User' },
  tags:   { type: 'array', of: 'uuid', ref: 'Tag' },
})

const posts = await Post.findMany({
  include: { author: true, tags: true },
  take: 20,
})
// one SQL query, typed { author: User | null, tags: Tag[] }

Also included

The rest of the boilerplate.

Auth
JWT in a cookie or a Bearer token. requireAuth('admin') checks roles.
Cache
Redis or in memory, with cacheResponse() for GET routes.
Logging
pino, with a request ID on every line.
Scaffolder
create-efc-app writes a working app with user and admin routes.
Mailer
Gmail or any SMTP server, sent from a background job.
CLI
Route listing, migrations, seeds and a read-only database browser.

All of it is in the docs →

Changelog

Recent releases.

  1. 0.9.0

    efc db generate writes migrations from model changes. Plus efc db pull, efc import-prisma, pool settings and a relations guide.

  2. 0.8.0

    efc add: auth, portals, billing and more, copied into your app as source you own.

  3. 0.7.1

    The scaffolder generates defineTable models with foreign keys on PostgreSQL.

  4. 0.7.0

    Relations for defineTable: foreign keys, many-to-many, include. Query logging.

  5. 0.5.0

    Types inferred from schemas, defineTable, and an MCP server for the docs.

  6. 0.3

    Auth routes and requireAuth('role'), typed schema fields, default operators and sequences.

  7. 0.2

    User and admin portals, RBAC, the route docs dashboard and the mailer in the scaffolder.

  8. 0.1

    First release: file-based routing, clustering, background tasks and JWT auth.

Next: defineCollection for MongoDB. After that, plugins, WebSockets and OpenAPI for 1.0.

Write the part
only you can write.

The router, the cluster, the queue and the data layer are done. Start with one command.

$ npx create-efc-app my-api
Star on GitHub →