neode
neode is a Neo4j OGM for Node.js designed to take care of the CRUD boilerplate involved with setting up a graph database project. Just install, define your models and go.
It runs against any Cypher-compatible engine: Neo4j (Community, Enterprise, Aura) and Memgraph over Bolt, or the embedded Relate engine in-process.
Coming from neode 0.4.9? Your existing map-based schemas keep working — read the migration guide.
1.0 alpha. This branch is the1.0.0-alpha.0prerelease line. Install it with thenextdist-tag —npm install neodestill gives you 0.4.9 until 1.0 goes stable.
Quickstart
Four steps: install, connect, define a model, round-trip.
1. Install
npm install neode@next
Requires Node.js 20 or newer (Neo4j 5.x / 2025.x servers, neo4j-driver 6).
2. Connect
// index.ts
import { fromEnv } from 'neode'
// Reads NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD, NEO4J_DATABASE (and the
// rest of the NEO4J_* set) — or from a .env file
const neode = fromEnv()
Already have a driver, or want the embedded engine? Every connect option is real:
import neo4j from 'neo4j-driver'
import Neode from 'neode'
const driver = neo4j.driver('bolt://localhost:7687', neo4j.auth.basic('neo4j', 'password'))
const neode = Neode.forDriver(driver) // over a neo4j-driver Driver
// Neode.forRelate(database) // or: embedded Relate engine, in-process
3. Define a model — pick your style
Two definition APIs. They feed the same metamodel and the same registry, so they are interchangeable — pick one per project or mix freely, and every read/write path behaves identically either way (parity between the two is covered by the golden-parity test suite).
// Decorators — a plain class, standard TC39 decorators
import { NodeEntity, Property, Uuid } from 'neode'
@NodeEntity()
export class Person {
@Uuid({ primary: true }) // auto-generated UUID primary key
id: string
@Property({ type: 'string', unique: true, required: true, min: 2, max: 50 })
name: string
@Property({ type: 'int' })
age: number
}
// Map-based — no decorators, same result
import { defineModel } from 'neode'
export const Person = defineModel('Person', {
id: { type: 'uuid', primary: true },
name: { type: 'string', unique: true, required: true, min: 2, max: 50 },
age: 'int', // shorthand: property : type
})
Both return a class you can pass to the facade, repositories and services below.
4. Round-trip
// Create — validated against the model before the write
const adam = await neode.create(Person, { name: 'Adam', age: 30 })
// Read — by primary key, or by filter
const found = await neode.find(Person, adam.id)
const people = await neode.get(Person, { age: 30 })
// Update
await neode.update(adam, { age: 31 })
// Delete
await neode.delete(adam)
Then create the constraints and indexes the model declares (unique, index, …):
await neode.installSchema() // CREATE CONSTRAINT … IF NOT EXISTS / CREATE INDEX … IF NOT EXISTS
await neode.dropSchema() // reverses installSchema() exactly
exists constraints need Neo4j Enterprise — set NEO4J_ENTERPRISE=true or call neode.setEnterprise(true).
That's the whole loop. Everything else the library does is in the matrix below.
Model definitions are process-global. Models registered through decorators,defineModel()orneode.model()/with()live in one shared registry, so everyNeodeinstance in a process sees them. One set of models per process is the intended shape; run separate processes for separate schemas.
Feature matrix
| Area | What you get | Details |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| Property types | any, string, number, int, integer, float, boolean, uuid, date, time, datetime, localtime, localdatetime, point, duration — plus array, defaults, primary / unique / index, hidden / protected / readonly, onCreateSet / onMatchSet / alwaysSet | Entities & decorators |
| Validation | Zod-powered rule names in the property config (required, min / max, email, regex, uuid, …). Every write path — save, create, update — is validated, with all failures collected into one error | Validation rules |
| Relationships | @OneToOne / @OneToMany (eager loading), relationship entities with @StartNode / @EndNode, relate() / detach() with edge properties, cascade delete options | Relationship decorators |
| Read-model decorators | @RelatedOne, @RelatedMany, @RelationshipCount, @RelationshipExists, @LabelPresent, @Computed, @Expression — projected into the read query, one query per read | API reference |
| Repositories | Repository — find, findAll, count, save, patch, delete, increment, countBy, deleteAll — plus custom @EntityRepository subclasses (TypeORM parity) | Repositories |
| Transactions | inReadTransaction / inWriteTransaction callback form, openReadTransaction / openWriteTransaction handle form, and batch() with per-statement errors | Transactions |
| Events | EventType.NODE_CREATED / NODE_UPDATED / NODE_DELETED, RELATIONSHIP_CREATED / RELATIONSHIP_MERGED, QUERY_EXECUTED — emitted synchronously, scoped to the instance, inside and outside transactions | Events |
| Schema sync & drift | installSchema() / dropSchema() with deterministic names, inspectSchema(), and the drift workflow: schemaDrift() → formatSchemaDrift() → applySchemaDrift() | Schema sync |
| Full-text search | search() over a full-text index, toFulltextQuery() to escape user input — hydrated entities with relevance scores | kNN & full-text search |
| Vector search | @Vector({ dimensions }) on a property + knn() k-nearest-neighbour search (Cypher 25 SEARCH clause with a procedure fallback) | kNN & full-text search |
| Pagination | Offset mode { page, limit, order? } and cursor/keyset mode { limit, orderBy, after?/before? } on a temporal property, both via neode.paginate() | The Neode facade |
| Bulk writes | createMany() / mergeMany() — chunked UNWIND writes, one managed transaction per chunk, rows validated like single creates | The Neode facade |
| Querying | Fluent Builder (all values parameterised), raw neode.cypher(), hydrate / hydrateFirst / toCollection, toJson() / jsonSafe() serialisation | Query builder |
| Error taxonomy | Typed NeodeError base, ConstraintViolationError (with parsed label / property), TransientError (retryable), classifyError() / isTransientError() | API reference |
| Agent ontology | ontology() / ontologyMarkdown() — emit the live graph schema for grounding Text2Cypher prompts or MCP resources | API reference |
| Testing & ops | wipe(), testId(), data factories (neode.factory()), verifyConnectivity() / waitUntilReachable(), splitCypherStatements() / runCypherScript() | API reference |
kNN & full-text search
import { knn, search, toFulltextQuery } from 'neode'
const runner = neode.getRunner()
const vectorHits = await knn(runner, 'Person', 'person_plot_vector', vector, 5)
const textHits = await search(runner, 'Person', 'person_plot', toFulltextQuery(query))
Indexes are declared on the model — @Vector({ dimensions: 1536 }) for vectors (installed by installSchema()), fulltext: true on a property or installFulltextIndexes(definitions) for full-text.
Docs
| Document | Contents | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | docs/api-reference.md | Full API — decorators, map schemas, facade methods, transactions, events, repositories, validation, connection providers | | docs/migration-guide.md | Migrating from neode 0.4.9 — IDs, facade signatures, sessions, batch, cascade, and what's new |