base
base is a small base class for Node applications. You extend it, and you get a namespaced key/value store with dot-path access (set, get, has, del, union, clear), events on every store operation, a define method for non-enumerable properties, an is method that stamps type flags such as isCollection onto the instance, and a use method that runs plugin functions against the instance and records them so the same plugin cannot register twice. It was the shared foundation of the Toolkit suite (assemble, verb, generate, update, templates) around 2015 to 2018. Version 3.0.0 is an ES class that extends cache-base and is the last release, published in March 2018. The important thing to understand about this package is the gap between its download count and its actual use: nearly all of those installs are version 0.11.2 from 2017 arriving as a transitive dependency of old build tooling, not people choosing it.
Do not install this. It has not shipped since 2018, its README documents an API the current version removed, and the download count that makes it look popular is a 2017 version being dragged along by old build tooling. If you found it in your lockfile, upgrade whatever depends on micromatch 3; if you were about to add it, a plain class plus dot-prop covers it.
Use it if
- You maintain something already built on the Toolkit suite (assemble, verb, generate, update) and need to understand or patch the base class it inherits from
- You are reading a stack trace that goes through base and want to know what the instance in front of you actually is: a cache-base subclass with a plugin registry bolted on
- You want a reference implementation of the idempotent-plugin idea, where use('name', fn) checks a registry and silently skips a second registration, because that specific pattern is well done and worth borrowing
- You are auditing a dependency tree and need to establish whether the base in your lockfile is the 2017 transitive copy or something a developer installed on purpose
- You are starting anything new. The last publish was 2018-03-30 and the repository's last push was 2022-10-28. There is no TypeScript declaration file, no ESM build, and no activity to suggest either is coming
- You believe the download number means people use it. npm's per-version breakdown for the same week shows about 99.7 percent of installs are 0.11.2, published in September 2017, pulled in by snapdragon 0.8.x through the old micromatch 3, braces 2, and nanomatch chain that still hangs off legacy build tooling. The current 3.0.0 gets on the order of 1,300 downloads a week. This is a package almost nobody installs deliberately
- You plan to follow the README. It documents Base.extend(MyApp) for inheritance, and that static method does not exist in 3.0.0, which uses ordinary class extends. Its headline usage example says app.set('foo', 'bar') then app.foo returns 'bar', and in 3.0.0 the value lands on app.cache while app.foo is undefined. The docs describe the 0.x API
- You want any of the pieces on their own. Dot-path get and set is dot-prop or lodash. Events are the standard EventEmitter. A plugin system with real hooks is tapable or hookable. Non-enumerable properties are Object.defineProperty. Each of those is one line of standard JavaScript or a maintained package, and base is a bundle of them from before that was easy
- You care about tree size for what you get. Installing base pulls 39 transitive packages for a class with six methods of its own, most of the weight coming through cache-base and its own chain of single-purpose modules from the same era
- You need help. 105 stars, 10 open issues out of 13 open issues and PRs, a Gitter badge pointing at a dead chat service, and a documentation site at base.github.io that returns 404
Setup reality
npm install base still works and installs cleanly: 39 packages, about 1.2 MB on disk, and npm audit currently reports zero vulnerabilities on a fresh tree, so this is a stale package rather than a dangerous one. It is CommonJS only, so require('base') from a .mjs file needs a default import and there is no named export. The engines field says Node 6 and up, which tells you when it was last considered. There are no type definitions on npm or in DefinitelyTyped, so a TypeScript project gets an implicit-any import. The behavior you have to internalize before writing a line: the default cache namespace in 3.0.0 is 'cache', so everything set through app.set lives at app.cache and never on the instance root, which contradicts both examples at the top of the README. Base.namespace('other') gives you a constructor that stores under a different property name. If instead you are here because base showed up in your lockfile, you did not install it: run npm ls base to find the parent, and it will almost always be snapdragon under micromatch 3 or braces 2. The fix is upgrading whatever pulls in micromatch 3, since micromatch 4 dropped snapdragon entirely and takes the whole subtree with it.
Patterns
Work out why base is installed at allfind-it-in-your-tree
npm ls base
# my-app@1.0.0
# └─┬ some-old-tool@2.3.0
# └─┬ micromatch@3.1.10
# └─┬ braces@2.3.2
# └─┬ snapdragon@0.8.2
# └── base@0.11.2
# who pulls the whole legacy chain
npm ls micromatch snapdragon
# yarn / pnpm equivalents
yarn why base
pnpm why baseIf the tree shows 0.11.2 you did not choose this package and nothing you write against the 3.0.0 API applies. snapdragon 0.8.x is the usual parent, reached through micromatch 3, braces 2, nanomatch, or extglob. micromatch 4 replaced that entire parser with picomatch and has no snapdragon dependency, so upgrading the top-level tool that pins micromatch 3 removes base, snapdragon, cache-base, define-property, and a dozen more in one step.
Basic usage, and where the values actually gohello-world
const Base = require('base');
const app = new Base({ foo: 'bar' }, { verbose: true });
app.get('foo'); //=> 'bar'
app.foo; //=> undefined (NOT on the instance)
app.cache; //=> { foo: 'bar' }
app.options; //=> { verbose: true }
app.set('a.b.c', 1);
app.get('a.b.c'); //=> 1
app.cache; //=> { foo: 'bar', a: { b: { c: 1 } } }The README's first example claims app.set('foo', 'bar') makes app.foo return 'bar'. In 3.0.0 it does not: the default cache-base namespace is 'cache', so everything lands on app.cache. This is the single biggest discrepancy between the published documentation and the published code, and it is why most snippets you find for this package fail immediately.
Store values under a different propertycustom-namespace
const Base = require('base');
const Store = Base.namespace('data');
const app = new Store();
app.set('user.name', 'Ada');
app.data; //=> { user: { name: 'Ada' } }
app.cache; //=> undefined
// namespace() returns a fresh constructor each call,
// with its own list of global plugins
const A = Base.namespace('a');
const B = Base.namespace('b');Base.namespace(name) builds and returns a new class rather than configuring the existing one, so two calls give you two unrelated constructors and an instanceof check against one fails for the other. The default export is namespace() called with no argument, which cache-base resolves to 'cache'. Nothing stops you naming the namespace something that collides with a method, and nothing warns you when it does.
Plugins are functions, and use() deduplicates by nameplugins
const Base = require('base');
const app = new Base();
function logger(app) {
app.define('log', msg => console.log(`[${app.type}] ${msg}`));
}
// named registration: the second call is a no-op
app.use('logger', logger);
app.use('logger', app => { app.define('log', () => 'overwritten'); });
app.log('hi'); //=> [app] hi
app.registered; //=> { logger: true }
// arrays and extra arguments both work
app.use([pluginA, pluginB]);
app.use(pluginC, { depth: 2 }); // options reach the plugin as arg 2Only the string-named form deduplicates. app.use(logger) without a name runs every single time, which is the mistake that produces duplicate event listeners in long-lived processes. The plugin is called with the instance as both `this` and the first argument. The registry lives on app.registered, defined as non-enumerable, so it will not show up in JSON.stringify or Object.keys.
The guard plugin authors are supposed to writeis-registered
function myPlugin(app) {
if (app.isRegistered('myPlugin')) return;
// safe to mutate: this only runs once per instance
app.define('render', str => str.toUpperCase());
}
const app = new Base();
app.on('plugin', name => console.log('registered', name));
app.use(myPlugin);
app.use(myPlugin); // guard short-circuits the second callisRegistered(name) has a side effect: unless you pass false as the second argument it records the name and emits a 'plugin' event, so calling it twice returns false then true. That is intentional and is what makes the one-line guard work, but it means isRegistered is not a pure query and using it to inspect an instance changes it. Pass false explicitly when you only want to look.
Subclassing in 3.0.0 (Base.extend is gone)inheritance
const Base = require('base');
class Collection extends Base {
constructor(cache, options) {
super(cache, options);
this.is('collection');
this.define('items', []);
}
add(item) {
this.items.push(item);
this.emit('add', item);
return this;
}
}
const c = new Collection();
c.type; //=> 'collection'
c.isCollection; //=> true
c.isApp; //=> undefined (is() clears it for any non-'app' type)typeof Base.extend is undefined in 3.0.0. The README still shows Base.extend(MyApp) with a Base.call(this) constructor, which is the 0.x pattern built on class-utils, and copying it throws immediately. Use standard class extends and remember to call super before touching this. Note also that is() deletes isApp when you set any other type, so code branching on isApp in a subclass silently stops matching.
Attach methods without polluting serializationdefine-non-enumerable
const app = new Base();
app.define('render', (str, locals) => str.replace(/{{(\w+)}}/g, (_, k) => locals[k]));
app.define({ a: 1, b: 2 }); // object form visits each key
Object.keys(app); //=> [ 'cache', 'options' ]
JSON.stringify(app); //=> {"cache":{},"options":{}}
app.render('hi {{name}}', { name: 'Ada' });define is Object.defineProperty with writable and configurable set, so it is for behavior and internal state that should not be serialized or iterated. Dot notation is explicitly not supported: define('a.b', 1) creates a property whose literal key is 'a.b'. Use set for data and define for methods, which is the convention every Toolkit plugin follows.
Every store operation emitsevents
const app = new Base();
app.on('set', (key, value) => console.log('set', key, value));
app.on('get', key => console.log('get', key));
app.on('del', key => console.log('del', key));
app.set('a', 1); // set a 1
app.get('a'); // get a
app.del('a'); // del aThe emitter comes from cache-base, which uses @sellside/emitter rather than Node's EventEmitter, so there is no removeAllListeners, no setMaxListeners, and no warning when listeners accumulate. The 'get' event fires on every read, including reads from inside the library itself, so attaching a logger there is noisy and measurably slow. There is no way to unsubscribe a plugin's listeners other than tracking the functions yourself.
What you inherit from cache-basestore-methods
const app = new Base();
app.set({ x: 1, y: 2 }); // object form
app.union('tags', 'a');
app.union('tags', ['b', 'c']); // => ['a','b','c'], deduplicated array append
app.default('retries', 3); // only sets if unset
app.prime('list', []); // ensure a value exists, return it
app.visit('set', { p: 1, q: 2 });
app.has('x'); //=> true
app.hasOwn('x'); //=> true
app.keys; //=> [ 'x', 'y', 'tags', 'retries', 'list', 'p', 'q' ]
app.size; //=> 7
app.del('x');
app.clear(); //=> cache is now {}None of these are documented in the base README because they belong to cache-base, so the base docs undersell what the class does. keys and size are getters, not methods, so app.size() throws. union always produces an array and deduplicates, which is convenient until you actually wanted duplicates. clear empties the whole namespace with no event per key.
The base getter walks up to the first ancestorparent-chain
const first = new Base();
first.set('config.env', 'prod');
const second = new Base();
second.parent = first;
const third = new Base();
third.parent = second;
third.base.get('config.env'); //=> 'prod'
third.base === first; //=> truebase is a getter that recurses through parent until it finds an instance with none, and it defaults to the instance itself when parent is unset. Nothing sets parent for you: you assign it by hand, and nothing checks for cycles, so a.parent = b with b.parent = a recurses until the stack overflows. It is also a plain writable property, so any plugin can reassign it.
Static use() applies to every future instanceglobal-plugins
const Base = require('base');
Base.use(app => {
app.define('createdAt', Date.now());
});
new Base().createdAt; //=> 1754...
// scoped to one constructor instead of the shared default export
const App = Base.namespace('data');
App.use(app => app.define('scoped', true));
new App().scoped; //=> true
new Base().scoped; //=> undefinedStatic use on the default export mutates a module-level array, and require caches the module, so a plugin registered by any package in the process affects every consumer's instances for the rest of its life. Instances already created are unaffected, only new ones. If you are writing a library, register on your own Base.namespace(...) constructor rather than on the shared default.
What the pieces look like without basereplace-it
// dot-path store
const { getProperty, setProperty, hasProperty } = require('dot-prop');
const cache = {};
setProperty(cache, 'a.b.c', 1);
getProperty(cache, 'a.b.c'); //=> 1
// events
const { EventEmitter } = require('node:events');
class App extends EventEmitter {}
// non-enumerable method
Object.defineProperty(app, 'render', { value: fn, configurable: true });
// idempotent plugins, the one idea worth keeping
class App2 extends EventEmitter {
#plugins = new Set();
use(name, fn) {
if (this.#plugins.has(name)) return this;
this.#plugins.add(name);
fn(this);
return this;
}
}This is the honest replacement for a package that has not shipped in eight years: about fifteen lines of standard JavaScript, no transitive tree, and types that your editor understands. dot-prop is ESM only from version 7 onward, so pin 6.x if you are still on CommonJS. The named-plugin dedupe is the one genuinely nice idea in base and it is five lines with a Set.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| dot-prop | npm | The only thing you wanted was get, set, has, and delete against dot-separated paths on a plain object, with no class to extend |
| cache-base | npm | You genuinely want the namespaced, event-emitting store, since that is the class base extends and it is one layer less indirection |
| tapable | npm | What you actually need is a plugin system, with named hooks, sync and async variants, and control over how results combine, rather than functions that mutate an instance |