empiric-node
v0.10.4
Published
Scaffold a Node/Express API and copy backend modules into it - own the source, not a dependency.
Maintainers
Readme
empiric-node
Scaffold a Node/Express API, then copy backend modules into it. The code lands in your repository as ordinary source you own and edit - there is no runtime dependency on this tool, and no library update can change your project without you.
npx empiric-node@latest init my-api # an empty Express skeleton
cd my-api
npx empiric-node@latest add mongoose-connection auth # MongoDB + register/login guardFor the fastest setup, accept the safe defaults and install dependencies automatically:
npx empiric-node@latest init my-api --yes
cd my-api
npm run devLeave off --yes for guided setup. Both starting points ask for TypeScript or JavaScript,
layered or module-based source, and show npm as the only available package manager. pnpm, Yarn,
and Bun are visible but disabled until their initialization flows are implemented. The empty
skeleton also asks whether to create a MongoDB connection; the fully wired choice adds no product
or database modules. Passing a directory names the project, so it is not asked for again. Nested
targets such as apps/orders-api are supported, and --no-install writes the project without
installing dependencies.
Thirty-five modules across eight categories - list prints them, info <module> explains one.
Commands
| | |
| ----------------- | ---------------------------------------------------------------- |
| init [dir] | Create a project. Asks what to start with, language and manager. |
| add <module...> | Copy modules into the project you are in. |
| list | Everything in the registry, grouped by category. |
| info <module> | What one module writes, needs and costs. |
| search <query> | Find a module by name, description, framework or database. |
| sync-env | Regenerate reusable config from .env and .env.example names. |
What init writes
By default, nothing you would not have typed yourself: package.json, tsconfig.json,
an app.ts with one route, and a server.ts that listens and shuts down cleanly. No modules
are copied. The interactive setup asks how future modules should be organised:
| Structure | Command | Where copied files go |
| ----------------- | --------------------------------- | -------------------------------------------------- |
| Layered (default) | init my-api --structure layered | the role folder each file names - src/routes/, |
| | | src/controllers/, src/models/, src/utils/, … |
| Module-based | init my-api --structure module | the same, except a feature's own routes and |
| | | controllers, which go to src/modules/<feature>/ |
The two differ over one thing: whether a feature's own HTTP surface sits in a folder named after the feature.
add login --structure layered add login --structure module
src/ src/
├── controllers/ ├── modules/
│ └── login.controller.ts │ └── login/
├── models/ │ ├── login.controller.ts
│ └── user.model.ts │ └── login.routes.ts
├── routes/ ├── models/
│ └── login.routes.ts │ └── user.model.ts
└── utils/ └── utils/
├── auth-token.ts ├── auth-token.ts
└── password.ts └── password.tsA feature's folder holds the feature, not a second copy of the layered tree - there is no
modules/login/controllers/. And only a feature gets a folder at all: a component owns
one when it ships routes or controllers. pagination, errors, cors and the other shared
building blocks own no route, so add pagination writes src/utils/pagination.ts in both
structures.
The model and the password helper stay shared on purpose. Four components ship the same
user.model.ts; putting it inside login/ would make register copy its own.
The generated folder layout records the choice without a tool-specific metadata file. Every
later add detects that layout, including generated app.ts imports and copied test imports,
so one project cannot drift between both structures. With --yes, layered is the default.
Use init . to initialize the current empty folder. Its folder name is normalized to a valid
lowercase npm package name. Choosing MongoDB copies one Mongoose connection helper, adds a local
DATABASE_URL to .env and .env.example, connects before app.listen(), and prints
MongoDB connected after the connection succeeds.
--preset node-api builds the other one instead: generated environment config, Pino and
pino-http request logging, and GET /health, mounted on the same Express server with graceful
shutdown. It contains exactly the config, logging, and health-check modules and asks no
database or authentication questions. Interactively, init offers the choice; --yes takes the
skeleton.
Either way the result is the same kind of project: add grows the skeleton into the wired
one module at a time, and a project built by init --preset node-api is indistinguishable
from a skeleton that added those three modules.
Authentication - one module, or four
auth is the whole flow in one copy: register, login, and the middleware that turns
Authorization: Bearer into req.userId. --with=session swaps the JWT for server-side
sessions on express-session.
npx empiric-node add mongoose-connection auth # the lot
npx empiric-node add mongoose-connection auth --with=session # sessions instead of a JWTThe same endpoints also exist as four modules that own one route each, for a project that wants sign-up without a password reset, or a reset flow whose token storage it intends to rewrite:
| Module | Route | What it does |
| ----------------- | ---------------------------- | -------------------------------------------------------- |
| register | POST /auth/register | Creates the user, bcrypts the password, returns no token |
| login | POST /auth/login | Exchanges email + password for a 15-minute JWT |
| forgot-password | POST /auth/forgot-password | Mints a single-use reset token, stores only its SHA-256 |
| reset-password | POST /auth/reset-password | Spends the token, then writes the new bcrypt hash |
npx empiric-node add register login forgot-password reset-passwordThe four ship a byte-identical User model and password helper, so add writes each once
and leaves it alone on the module that asks for it second. auth's User model is not the
same file - take auth or take these, because installing both asks you to choose between
two models and leaves two versions of the same routes.
Each answers uniformly whether or not an address exists, so none of them can be used to work out which emails have accounts.
Add the smaller access and messaging building blocks independently:
npx empiric-node add authorization api-key-auth
npx empiric-node add email email-verificationThe email modules share a byte-identical service file, so selecting both writes one SMTP implementation. Email verification keeps its state in a separate collection and does not replace the project's User model.
Hosted-provider authentication is one middleware per provider:
npx empiric-node add google-auth
npx empiric-node add firebase-auth
npx empiric-node add supabase-authEach verifies the provider's current token format with its official server SDK and writes the
verified subject to request.userId, ready for the separate authorization guards.
What add works out for itself
It prints this as its first line, so you can see what it decided before it writes anything:
TypeScript (tsconfig.json) · src/ · Layered · npm · ESM- Language - a
tsconfig.json, thentypescriptin package.json, then the balance of.tsto.jsundersrc/. It asks when a folder is empty.--ts/--jsoverrule it. - Source root -
src/. - Structure - from an existing layered,
src/modules/, or legacysrc/features/tree. Existing vertical roots are preserved. On the first interactiveadd, the CLI asks when it cannot tell. Use--structure moduleexplicitly in scripts. - Package manager -
initcurrently creates npm projects only. When adding to an existing project, the CLI still reads its lockfile orpackageManagerfield so it does not mix npm into a project that already uses another manager. - Module system - module source is ESM. A CommonJS project gets told so rather than getting files that will not load.
Dependency versions are part of the registry contract. If a module declares zod@^4.0.5,
add installs the compatible baseline [email protected] instead of requesting bare zod and whatever
future version carries the latest tag. An existing compatible dependency is left untouched;
an incompatible or unbounded declaration stops with guidance instead of being replaced. Commit
the package manager's lockfile so CI and deployments reproduce the exact resolved dependency tree.
add also updates .env and .env.example. Existing values are never replaced or
duplicated. Required names such as JWT_SECRET are added empty for you to fill, safe defaults
are applied, and optional names are added as comments so installing a module cannot silently
change runtime behaviour.
add config also generates src/config/app.config.ts (or .js) from those environment names.
Active assignments in .env.example are required, commented assignments are optional, and names
found only in .env are included without copying their values. Names are case-sensitive and retain
their spelling, so a valid lowercase assignment such as tt=... generates config.tt.
When and how to run sync-env
Run it from the project root after manually adding, removing, renaming, commenting, or uncommenting
an assignment in .env.example or .env:
# 1. Save the environment-file changes.
# 2. Refresh the generated key list.
npx empiric-node@latest sync-env
# 3. Review src/config/app.config.ts (or .js), then import config from it.Do not run it immediately after add config; the initial generated config is already created. You
also do not need it after add <module> because later module additions resync automatically after
updating the environment files. Run it for environment-name changes made by you, a teammate, a
merge, or a branch switch when the generated file needs to catch up.
Generator-owned files update in place. A hand-written app.config is protected unless
sync-env --force explicitly replaces it; keep project-specific number, boolean, URL, and
cross-field parsing in a separate config file.
Components may declare other components they require. add resolves those prerequisites
first, discovers existing components from their primary source files, and copies each one once.
For example, npx empiric-node add register automatically supplies and wires
mongoose-connection; email-verification supplies both the database and email services.
Provider components also print their remaining setup steps and official console/documentation links after copying, so credentials and the first protected route are not hidden in a README.
In a JavaScript project you get the same modules with the types stripped - generated by the library's own build, formatted by its Prettier config, and with the type-only files left out rather than written as empty ones.
Files you already have
add never overwrites silently. If a module's primary file already exists, the interactive
CLI first offers Keep existing, Review update, or Reset module. In scripts,
re-adding stops with guidance; --force explicitly resets it.
During a reviewed update, a file whose bytes already match is left alone; one that differs is asked about individually:
src/models/user.model.ts already exists and differs.
› Replace overwrite it with the registry version
Rename write the new one beside it, keep yours
Skip keep yours, write nothing--force answers Replace for all of them. --yes accepts the other prompts - installing
dependencies, mounting routes - but never this one: losing a file you edited stays an
explicit decision.
Wiring
A module that mounts something declares where it belongs, so add can offer to put it in
src/app.ts in the right place relative to whatever is already there - a route above the
error handler, the request logger above the routes. Single-line and formatted multi-line
imports are kept intact; a new module import is placed after the complete leading import
block. When your app.ts has grown past the shape it can recognise, it prints the lines
instead of guessing.
Lifecycle components use the same rule for src/server.ts. For example, the Mongoose
connection is offered immediately before app.listen(). Replace, rename and skip remain
available; an unfamiliar server shape gets a paste-ready snippet instead of a guessed edit.
init --preset node-api composes the whole file the same way from config, logging, and health
check. The skeleton ships its own app.ts - there is nothing to compose - and add inserts into
it.
Tests
A module ships the tests that document its behaviour, and --with-tests copies them into
the project with the module:
npx empiric-node add pagination --with-tests
npm testThey land in tests/, with every import pointed at wherever the source actually went - so
the same command works in a layered and in a module-based project, in TypeScript and in
JavaScript. The test runner, and anything else the test files import, are added to
devDependencies; a project with no test script gets vitest run, and one that already
has its own is left alone.
Tests can also be added after the module source is already present:
npx empiric-node add firebase-auth --with-tests --yes
npm testThis additive re-run keeps the installed module source byte-for-byte, copies only missing tests,
preserves any test file the project already edited, and installs the missing test dependencies.
Leave off --yes to see the existing-module prompt; its default Keep existing choice has the
same source-preserving behavior when --with-tests was requested. Choose Review update only
when the tests need a source extra that is not installed yet.
A test file covers the whole module rather than the default copy of it, so it may import an
extra you did not ask for. --with-tests copies the files that run against your copy, and
pulls an extra in only when nothing else would run:
add auth --with-testscopies the JWT suite and leavessession.test.tsbehind, rather than dragging a second login strategy into the project.add pagination --with-testscopiespagination.cursor.tstoo, because the module's one test file imports it and the alternative is copying no tests at all.
Either way the CLI says what it did, and every copied file is tagged with the --with= that
brought it.
Copied tests are a starting point, not a guarantee. They are yours to edit as the code diverges from the version you copied, and a later keep-existing test copy will not overwrite those edits.
Options
Nothing below is required to use the CLI: init and add ask before they change anything.
Reach for a flag when you are scripting the CLI, or when you want to disagree with what it
detected.
--with <extra> Opt-in extras, e.g. --with=cursor (add)
--with-tests Copy the module's tests and what runs them (add)
--ts, --js Force the language instead of detecting it
--structure <type> layered | module (init or first add)
--yes, -y Accept every prompt except overwriting files
--force Reset module source, or replace app.config (add/sync-env)Advanced - scripting, CI and self-hosting:
--dry-run Show what would be written, write nothing (add)
--no-install Print the install command instead of running it
--preset <name> node-api-minimal (default) | node-api (init)
--pm <manager> npm only; other managers are disabled (init)
--database <type> none | mongodb (init, empty skeleton only)
--category <name> Filter the listing (list)
--registry <url> Registry URL or directory
--cwd <dir> Run as if started in this directory
--help, -h Show global or command-specific help
--version, -v Show the CLI versionRegistry
Modules are read from a registry, resolved in this order:
--registry- a URL or a local directoryEMPIRIC_REGISTRY_URL- the copy bundled in this package
Only use registries you trust: they supply source code and dependency names. The CLI validates their schema and refuses absolute or parent-directory file targets before writing anything. Remote registry requests time out after ten seconds.
The bundled copy is the default, so init and add need no network and no server;
the trade-off is that new module versions arrive with a CLI release. Point any of the
two overrides at a hosted registry to track it independently:
EMPIRIC_REGISTRY_URL=https://example.com npx empiric-node add paginationNo project metadata file
init and add do not create a CLI manifest, lock file or branded metadata in the generated
project. Language, package manager and architecture are detected from standard project files
and directories. Installed components are recognised by their declared primary source paths.
Older empiric.json files remain readable for backward compatibility, but the CLI never creates
or updates them. They can be deleted when a project no longer needs their legacy overrides.
Developing it
From the library repository:
npm run cli:install # once
npm run registry:files # build a registry from modules/
npm run cli -- list --registry ./dist/registry
npm test # includes packages/cli/tests