npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

empiric-node

v0.10.4

Published

Scaffold a Node/Express API and copy backend modules into it - own the source, not a dependency.

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 guard

For the fastest setup, accept the safe defaults and install dependencies automatically:

npx empiric-node@latest init my-api --yes
cd my-api
npm run dev

Leave 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.ts

A 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 JWT

The 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-password

The 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-verification

The 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-auth

Each 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, then typescript in package.json, then the balance of .ts to .js under src/. It asks when a folder is empty. --ts / --js overrule it.
  • Source root - src/.
  • Structure - from an existing layered, src/modules/, or legacy src/features/ tree. Existing vertical roots are preserved. On the first interactive add, the CLI asks when it cannot tell. Use --structure module explicitly in scripts.
  • Package manager - init currently creates npm projects only. When adding to an existing project, the CLI still reads its lockfile or packageManager field 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 test

They 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 test

This 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-tests copies the JWT suite and leaves session.test.ts behind, rather than dragging a second login strategy into the project.
  • add pagination --with-tests copies pagination.cursor.ts too, 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 version

Registry

Modules are read from a registry, resolved in this order:

  1. --registry - a URL or a local directory
  2. EMPIRIC_REGISTRY_URL
  3. 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 pagination

No 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