Skip to main content

Introduction

mikro-orm-neo4j is a native Neo4j driver for MikroORM. Entities, the EntityManager, repositories, the unit of work and the identity map all behave the way MikroORM documents them. What changes is what runs underneath: a filter becomes a Cypher pattern, a many-to-many becomes an edge, and a populate becomes a traversal.

const books = await em.find(
Book,
{ price: { $gte: 10 }, author: { name: 'Ann' }, tags: { $some: { name: 'Fiction' } } },
{ populate: ['author'], orderBy: { title: 'ASC' }, limit: 20 },
);
MATCH (this0:Book)
WHERE this0.price >= $param0
AND EXISTS { MATCH (a:Author)-[:WROTE]->(this0) WHERE a.name = $param1 }
AND EXISTS { MATCH (this0)-[:TAGGED]->(t:Tag) WHERE t.name = $param2 }
OPTIONAL MATCH (this1:Author)-[:WROTE]->(this0)
RETURN this0 AS node, this1 AS rel_author
ORDER BY this0.title ASC LIMIT 20

What it adds​

Graph-native relationshipsDirected, typed edges (IN / OUT), traversal filters at any depth, and populate: ['$infer'] that is genuinely type-safe here.
Relationship entitiesEdges that carry properties of their own, modelled as ordinary entities on MikroORM's standard pivot path.
Query conditionsThe whole operator surface mapped onto Cypher — and the ones a graph cannot express refused by name rather than silently ignored.
Full-text searchA search field typed as the search backs a real Neo4j fulltext index: analyzers, weights, interface-wide indexes, GraphQL directive.
Streamingem.stream() reads through a Bolt cursor: bounded memory, populated relations merged as rows arrive, close() / asStream() / live stats.
Raw CypherA cypher tagged template whose interpolations are always named Bolt parameters, em.run<T>(), and virtual entities for read models.
Stored routinesAPOC, GDS and the built-ins declared once, called like any ORM method, composed into queries, and generated straight from the server.
Multi-tenancyMikroORM's schema option implemented as soft subgraphs, scoping writes, reads and both ends of every relationship.
Indexes & constraints@Index() / @Unique() materialized as real Neo4j indexes and constraints through orm.schema.ensureIndexes().
GraphQL SDLExport the metadata as SDL for @neo4j/graphql, comments included — schemas an LLM can actually read, and a full CRUD API on NestJS.
ObservabilityOne execution funnel, so debug, slowQueryThreshold, per-query labels and a custom logger all work with no driver-specific setup.

Plus dual-format publishing (ESM and CommonJS) and type augmentation of @mikro-orm/core, so the graph-specific options autocomplete without a single as any.

Requirements​

RequirementVersion
Node.js≥ 22 (the driver uses node:fs's globSync)
@mikro-orm/core7.x — pinned exactly, see stored routines
Neo4j≥ 5; ≥ 5.7 if you use schemas

What a graph changes​

Three things behave differently from the SQL drivers, and they are worth knowing before the first query:

  • There are no tables, so there is no DDL. A graph's schema is its indexes and constraints; orm.schema.ensureIndexes() creates them and is idempotent. There is no migration story to set up, and adding a property to an entity is not a migration.
  • A "pivot table" is the edge itself. Many-to-many stays on MikroORM's pivot path, so pivotEntity and relation properties behave as documented — but the join is a pattern match.
  • Anything the driver cannot honour is refused, loudly. Isolation levels, pessimistic locks, partial indexes, SQL fragments in a filter: each throws with a message naming the alternative, rather than being accepted and quietly doing nothing.

Where to start​

  1. Installation — the packages, the server, and the TypeScript setup.
  2. Quick start — an ORM instance, two entities and a query.
  3. Modeling the graph — labels, directions, edge properties, indexes.
  4. Querying — what each operator compiles to.