mingo: run MongoDB queries and aggregation pipelines against plain JavaScript objects
MongoDB query language for in-memory objects
At a glance
- What is it?
- mingo is a TypeScript implementation of the MongoDB query language that filters, projects, aggregates and updates in-memory objects with the same filter syntax you would write in a shell. It is MIT licensed, ships as ESM and CommonJS with its own types, and its operator set is a context you control, which is what makes tree shaking work.
- Who is it for?
- mingo fits a TypeScript or JavaScript codebase that has a list of plain objects in memory and needs to filter, group or sort them with the same expressions a MongoDB developer would already know, and it fits anyone who wants to share a filter definition between a client and a server without changing its shape.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The same filter syntax, evaluated in your process
mingo is described as a MongoDB query language for in-memory objects, and the pitch in one line is that you can use the MongoDB query language without MongoDB.
The basic example shows what that means.
import * as mingo from "mingo";
const result = find(
[
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 21 },
{ name: 'Charlie', age: 25 },
],
{ age: { $gte: 25 } }
).all()The filter is the object literal you would type into mongosh. The difference is that the input is a JavaScript array and the output is a JavaScript array, with no server, no wire protocol and no connection string.
That is more useful than it sounds for a specific class of problem. A browser application that has a list of records already in memory and needs to filter it the way the server would is a case where shipping a database or an HTTP round trip is absurd overhead. So is a server side function that receives a payload and needs to apply the same transformation the database would have applied, because the two code paths then behave identically and the filter literal can be shared.
The feature list names the coverage explicitly. Dot notation selectors, so <array>.<index> and <document>.<field>. Query and projection support. An aggregation pipeline with accumulators, expressions, window operators and variables, including $$ROOT, $$CURRENT, $$DESCEND, $$PRUNE, $$KEEP, $$REMOVE and $$NOW. Update support for collections and objects. A lazy data processing pipeline. And custom type value equality when toString is implemented.
That last one is a small detail with real consequences: an object with a toString method is compared by its string form rather than by reference, which is what you want when your records are class instances rather than plain literals.
Two import paths, and the context that decides which operators exist
The design decision that separates mingo from a naive implementation is the operator context, and it is the reason to read the source rather than just the API list.
Objects imported through the default entry point automatically add all operators into their context. That is the convenience path, and it means the whole operator set is available immediately.
The alternative is the base modules, mingo/{core,query,aggregator,updater}, plus importing individual operators and registering them into a custom Context.
import { Context } from "mingo/core";
import { Aggregator } from "mingo/aggregator";
import { $match, $count } from "mingo/operators/pipeline";
import { $gt } from "mingo/operators/expression/comparison";
const context = Context.init({
pipeline: { $count, $match },
expression: { $gt }
});
const agg = new Aggregator(
[{ $match: { score: { $gt: 80 } } }, { $count: "passing_scores" }],
{ context }
);The reason to do this is stated plainly: it is necessary if you use a bundler and want to exclude unused objects during tree shaking. A browser bundle that only ever runs a $match and a $count should not carry the entire MongoDB operator set because of one import line.
And there is a second, less obvious benefit: the context is what makes missing operators fail loudly. In the example, the comment says the only operators available will be $match, $count and $gt, and attempting to use any other operator will throw an error.
The difference is that with a context you have declared the contract yourself. A pipeline that references an operator outside your context fails at construction, which is a better time to find out.
One more property worth knowing: operators loaded into a Context are immutable. Registering the same operator name is a no-op and does not throw an error, and the stated reason is that this prevents unintentional errors.
Query, cursor, and the shape of the search API
The Query class serves two purposes that are worth separating, because they answer different questions.
The first is predicate testing. You build a query with criteria and ask whether a document matches, and the answer is a boolean.
import { Query } from "mingo";
let query = new Query({
type: "homework",
score: { $gte: 50 }
});
query.test(doc);The second is searching and filtering, where the query produces a cursor.
const query = new Query({ score: { $gt: 10 } });
const cursor = query.find(collection);
cursor.sort({ student_id: 1, score: -1 }).skip(100).limit(100);
for (const value of cursor) {
console.log(value);
}The cursor is where the lazy data processing pipeline in the feature list shows up. The sort, skip and limit calls are chained and return the cursor, and the results are pulled by iterating. The chaining reads like the MongoDB shell, and the iteration model means nothing is computed until you consume it.
That laziness is what makes mingo usable on a large array, because a $match stage can stop early rather than materialising an intermediate result set. It is also why the return type of find is a cursor rather than an array, and why the README's first example ends with .all() to force the materialisation when you want an array.
The default module exports the functions find, aggregate, update, updateMany and updateOne, and the classes Query and Aggregator. The README directs you to the module documentation for the full surface and to the official MongoDB documentation for the semantics of specific operators, which is a sensible division of responsibility. mingo is not where you learn what $unwind does; it is where you run it.
The $jsonSchema hole, and what the library refuses to guess
One feature is deliberately not implemented, and the way the library handles it says something about how it is designed.
To use the $jsonSchema operator, you must register your own JsonSchemaValidator in the options. No default implementation is provided out of the box, so users can use a library with their preferred schema format. The example uses Ajv.
const jsonSchemaValidator: JsonSchemaValidator = (s: AnyObject) => {
const ajv = new Ajv();
const v = ajv.compile(s as Schema);
return (o: AnyObject) => (v(o) ? true : false);
};
mingo.find(docs, { $jsonSchema: schema }, {}, { jsonSchemaValidator }).all();The reason for the hole is a good one. MongoDB's own JSON Schema implementation is specific, and the ecosystem has several schema libraries with different dialects and different capabilities. Bundling one would mean either a dependency the project does not want or a partial implementation that behaves differently from the server. So mingo delegates, and the contract is a function you provide.
The cost of that choice is that the failure mode is sharp. An error is thrown when the $jsonSchema operator is used without a jsonSchemaValidator configured. You do not get a silent pass, which is the right behaviour, but it does mean one more thing to wire up before a schema filter works.
The same pattern appears in the operator context. Both are cases where the library has made a deliberate boundary between what it can decide for you and what it refuses to guess, and in both cases the default is to fail rather than to approximate.
Aggregation over arrays, and the pipeline shape
The aggregation support covers the MongoDB pipeline stages, and the example in the README shows the granular import path in use.
import { Aggregator } from "mingo/aggregator";
import { Context } from "mingo/core";
import { $group } from "mingo/operators/pipeline/group";
import { $match } from "mingo/operators/pipeline/match";
import { $sort } from "mingo/operators/pipeline/sort";
import { $min } from "mingo/operators/accumulator/min";Note the import paths. Pipeline stages live under mingo/operators/pipeline, accumulators under mingo/operators/accumulator, and expression operators under mingo/operators/expression with a category folder, for example comparison. That three way split is what makes the tree shaking claim work, because a pipeline that only uses $group and $match should not pull in every accumulator.
The Aggregator is constructed with a pipeline and an options object carrying the context.
const agg = new Aggregator(
[{ $match: { score: { $gt: 80 } } }, { $count: "passing_scores" }],
{ context }
);
const result = agg.run([
{ _id: 1, score: 10 },
{ _id: 2, score: 60 },
{ _id: 3, score: 100 }
]);The result of run is the output of the pipeline, so a $count stage at the end collapses the results into a single document. That is the standard MongoDB behaviour and it is reproduced faithfully, which matters if the same pipeline is meant to run on both sides.
What is not reproduced is the engine around the pipeline. There is no index, no planner and no query cost model, because there is nothing to plan against: the input is an array you already hold. The performance characteristic follows from that, and the repository acknowledges it with a benchmarks/ directory and a perf script that runs the performance harness with npx tsx benchmarks/perf.ts. Any measurement there is of the library over in-memory data, not of it against a database.
Packaging, tooling, and what the repository says about itself
The package is version 7.2.5 and ships three builds. The main field points at a CommonJS entry, the module field at an ES module entry, and the types field at the generated declarations. The build script runs a clean and then a custom build.mjs, with a postbuild step running the TypeScript compiler over tsconfig.build.json, and a postinstall script that runs the same build.mjs in install mode.
The dev dependencies describe a serious setup. TypeScript 6.0.3, vitest 4.1.11 for the test run, esbuild 0.28.1 for the build, typedoc for the API documentation, eslint 9 with the typescript-eslint and import sorting plugins, prettier 3.6.2, husky 9.1.7 with lint-staged, and ajv 8.20.0 as the schema validator used in the $jsonSchema example.
The test script is vitest run with coverage, and the release script is npm run test and then npm run build and then a publish from inside the build directory, which means the published package is the build output rather than the source tree. The prepublishOnly script echoes a reminder to use the release script and exits false, which is a small guard against the wrong path.
The repository root has a .husky/ directory, a .prettierrc, an eslint.config.mjs, a vitest.config.mjs, a CHANGELOG.md, and a demo.html alongside build.mjs. The demo is not incidental, because the README points to a playground for trying the library in a fully functional environment, and the posttypedoc step runs the build script in demo mode, so the playground is regenerated with the documentation.
The licence is MIT, published as a LICENSE file at the root. For a library that you may end up bundling into a closed source product, that is the licence you want.
One detail in the docs matters for anyone tracking changes. There are two published guides, the module documentation and the playground, and both are hosted under kofrasa.github.io. The site is where the API reference lives, since the README explicitly says to refer to the official MongoDB website for information on how to use specific operators. The library is the engine; the semantics are borrowed deliberately.
Editorial conclusion
mingo fits a TypeScript or JavaScript codebase that has a list of plain objects in memory and needs to filter, group or sort them with the same expressions a MongoDB developer would already know, and it fits anyone who wants to share a filter definition between a client and a server without changing its shape. It does not fit a project that needs an index, a query planner or a database, because mingo evaluates the pipeline over an array in your process, and it does not fit a bundle that cannot afford a dependency of this size. Check three things before you adopt it. Check whether you want the whole operator set, because importing from the default entry point automatically adds all operators to the context, and the granular import path exists so that a bundler can drop the unused ones. Check that your MongoDB version's operators are covered, since the library documents its operator set against the MongoDB reference and gaps are the likely source of a surprising result. And check the areas where the library deliberately refuses to guess, most notably $jsonSchema, which throws unless you register your own validator. The last push was on 2026-10-15, the package version is 7.2.5, and the license is MIT.
Frequently asked questions
How do I install mingo?
Run npm install mingo. The package is version 7.2.5 and publishes a CommonJS entry, an ES module entry and its own TypeScript declarations, so no separate types package is needed.
What can mingo do that a plain JavaScript filter cannot?
It supports the MongoDB query language against in-memory objects, including dot notation selectors, query and projection, the aggregation pipeline with accumulators, expressions, window operators and variables, and update operations, plus a lazy processing pipeline and custom type equality when toString is implemented.
How do I load only the operators I need in mingo?
Import from the base modules such as mingo/core, mingo/aggregator and the individual operator paths, then register them in a custom Context with Context.init and pass that context in the options. Operators loaded into a Context are immutable, so registering the same name again is a no-op.
Does mingo support the $jsonSchema operator?
Only if you provide a validator. To use $jsonSchema you must register your own JsonSchemaValidator in the options, and no default implementation is provided, so you can use a library with your preferred schema format. An error is thrown when the operator is used without one configured.
How do I filter and sort a collection with mingo?
Create a Query with your criteria, call find on the collection to get a cursor, then chain sort, skip and limit on the cursor and iterate the results. The find function is also exported from the default module for the shorter form, ending with .all() to materialise an array.
What license is mingo released under?
MIT, with a LICENSE file at the root of the repository. The last push was on 2026-10-15, and the package version is 7.2.5.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/kofrasa-mingo)Was this useful?