Zero-config, plug-and-play MongoDB client management for Node.js. Point it at a connection with an env var (or nothing at all, if you've got a config file), and import collection/db straight from the package root — no init() call required.
- ✅ Zero-config: reads
MONGO_URI/MONGO_DATABASE(or a config file) automatically — no boilerplate to get started - ✅ Works with connection URIs, structured connection parts, or
MongoClientOptions - ✅ Support for multiple named, independently-configured connections (
getConnection) - ✅ One connection serves many databases — no need for a second connection just to reach a second database on the same cluster
- ✅ Everything is lazy: clients aren't constructed until first use, and the driver connects on first operation
- ✅
collection()works as a direct call, anawait-to-value, or a.then()chain — whichever reads best at the call site - ✅ Pino logging — bring your own logger, or get a sane default
- ✅ TypeScript support, including generic
collection<T>(name) - ✅ Direct access to
dbandcollectionhelpers from the package root
# NPM
npm install @notross/mongo-singleton
# Yarn
yarn add @notross/mongo-singletonSet an environment variable — that's it:
MONGO_URI="mongodb://localhost:27017/myApp"import{collection}from'@notross/mongo-singleton';// all three of these are equivalent — use whichever reads best:constuser=awaitcollection('users').findOne({email: 'john.doe@gmail.com'});constusers=awaitcollection('users');constsameUser=awaitusers.findOne({email: 'john.doe@gmail.com'});constgetUserByEmail=(email: string)=>collection('users').then((users)=>users.findOne({ email }));No init(), no connect(). If MONGO_URI includes a database path (as above), you don't even need a separate MONGO_DATABASE — it's pulled from the URI. Set both explicitly if you'd rather keep them separate:
MONGO_URI="mongodb://localhost:27017"
MONGO_DATABASE="myApp"MONGODB_URI / MONGO_URL and MONGODB_DATABASE are also recognized, to match common hosting-provider conventions.
If nothing is configured anywhere, the first collection()/db()/connect() call throws a clear error telling you what to set — never a silent connection to localhost or the driver's own test database default.
Prefer to configure it from code instead of the environment? One call, at boot:
import{configureMongoSingleton}from'@notross/mongo-singleton';configureMongoSingleton({connection: process.env.MONGO_URI,database: 'myApp'});For a given setting, highest wins:
- Explicit code —
configureMongoSingleton({...})/new MongoSingleton({...})/mongoClient.init({...}). Always available, always wins. - Environment variables —
MONGO_URI/MONGODB_URI/MONGO_URL,MONGO_DATABASE/MONGODB_DATABASE. - Config file — see below. A checked-in file is a repo default; it deliberately can't override an env var injected by your deploy environment.
- The connection URI's own path —
mongodb://host/myAppsuppliesmyAppas the database if nothing else did.
Optional. Powered by cosmiconfig, so any of these work: a mongoSingleton key in package.json, .mongosingletonrc(.json|.yaml|.yml|.js|.cjs), or mongosingleton.config.(js|cjs).
// mongosingleton.config.jsmodule.exports={uri: process.env.MONGO_URI,// default (unnamed) connectiondatabase: 'myApp',clients: {// named connections — see belowanalytics: {uri: process.env.ANALYTICS_MONGO_URI,database: 'events'},},};Two options if your app needs more than one distinct MongoDB connection.
import{MongoSingleton}from'@notross/mongo-singleton';exportconstclientA=newMongoSingleton({connection: process.env.URI_A,database: 'dbA'});exportconstclientB=newMongoSingleton({connection: process.env.URI_B,database: 'dbB'});getConnection ensures a single instance per connection ID across your app. With a clients map in a config file (above), it needs no code-side setup at all:
import{getConnection}from'@notross/mongo-singleton';const{ collection }=getConnection('analytics');// resolves from config file — zero args neededconstevents=awaitcollection('events').find().toArray();Or pass options explicitly:
getConnection('client-a',{connection: process.env.URI_A,database: 'dbA'});getConnection('client-b',{connection: process.env.URI_B,database: 'dbB'});// elsewhereconst{ collection }=getConnection('client-a');constaccount=awaitcollection('accounts').findOne({ email, password });Named connections also get a per-id environment override, independent of any config file: MONGO_<ID>_URI / MONGO_<ID>_DATABASE (id upper-cased, non-alphanumerics collapsed to _) — e.g. getConnection('analytics') reads MONGO_ANALYTICS_URI if set.
A second
getConnection('client-a', {...})call does not overwrite an already-created connection. To reconfigure, callclient.init(...)on the handle'sclient— unlike in v2, this now actually rebuilds the underlying connection.
const{ client }=getConnection('client-a');client.init({connection: '...',database: '...'});Note on naming:
getConnectionreturns a{ client, collection, db }handle.clientthere is the underlyingMongoSingleton/driver-managed connection — not a per-database accessor. If you only need to read/write documents, you'll almost always destructure justcollection/dband never touchclientdirectly.
A single MongoClient can safely serve multiple databases on the same cluster — no new sockets required. database is just the default for a given instance; override it per call:
constclient=newMongoSingleton({connection: process.env.MONGO_URI});client.collection('users');// uses the default databaseclient.collection('events',{database: 'analytics'});// same connection, different dbBacked by Pino. Bring your app's own logger (or a .child(...) of it) so every MongoSingleton instance logs through the same sink your app already uses:
importpinofrom'pino';import{MongoSingleton}from'@notross/mongo-singleton';constlogger=pino();constclient=newMongoSingleton({connection: process.env.MONGO_URI, logger });Omit logger to get a shared default Pino instance (level from LOG_LEVEL, defaulting to info), or pass logger: false to disable logging for that instance entirely. Each instance gets its own logger reference — configuring one connection's logging never affects another's.
Optional, opt-in — call once at boot if you want it:
import{registerShutdown}from'@notross/mongo-singleton';registerShutdown();// closes every getConnection()-registered client + the root client on SIGINT/SIGTERMnewMongoSingleton(opts?: MongoSingletonOptions);typeMongoSingletonOptions={connection?: ConnectionInput;database?: string;clientOptions?: mongodb.MongoClientOptions;logger?: import('pino').Logger|false;};typeConnectionInput=ConnectionProps|SparseConnectionProps|string;typeConnectionProps={prefix: string;// e.g., "mongodb://" or "mongodb+srv://"username: string;password: string;host: string;port?: number;defaultauthdb?: string;authSource?: string;options?: URLSearchParams;};typeSparseConnectionProps={uri: string};Methods:
init(opts)– (Re)initialize — covers both first-time setup and reconfiguration. Safe to call more than once; rebuilds the connection each time.collection<T>(name, opts?)– Typed, dual-mode collection handle (see call styles above).opts.databaseoverrides the instance default.db(database?)–Dbhandle fordatabase, or the instance default.connect()– Explicitly wait for a connection (not required beforecollection()/db()— the driver connects lazily on first operation). Useful for pre-warming at boot or wiring status logging.disconnect()– Closes the connection and resets state so the next call lazily reconnects.client– The underlyingmongodb.MongoClient, created lazily on first access.status/error– Current'disconnected' | 'connecting' | 'connected' | 'error'and the last error, if any.
mongoClient, db, collection, connect, configureMongoSingleton — bound to a single zero-arg MongoSingleton instance, resolved lazily from env/config on first use.
getConnection(id, opts?), disconnectAll(), registerShutdown(...extraClients).
This is a breaking release:
useClientwas renamed togetConnection— "client" reads as a per-database accessor to some, when what's actually being registered/retrieved is a distinct connection. The returned shape is unchanged ({ client, collection, db }).databasemoved from a required constructor field to an optional default, overridable percollection()/db()call.- Logging changed from a built-in custom logger (
logging/logLevelson connection props) to Pino dependency injection (loggeroption). The old logger mutated a shared global instance across every client — this fixes that. init()now actually rebuilds the underlying client — previously it silently updated internal fields but kept using the original (stale)MongoClient. The separateconfigure(clientOptions)method was removed as redundant (init({ clientOptions })covers it); use the new top-levelconfigureMongoSingleton(opts)to reconfigure the default instance from code.getDb/connectedDbwere removed; useawait mongoClient.connect()for the same "wait for a real connection" behavior.mongoClient.database(a cachedDbfield) was removed in favor ofdb()/collection(), which support multiple databases per client.collection()now returns a dual-mode handle (see call styles above) rather than a plainCollection— it's aProxyaround the real one, soinstanceof mongodb.Collection, property access, and method calls all behave identically; the only observable differences are an extra frame in stack traces and the added.then().