Define your graph in TypeScript. neode writes the Cypher.
A TypeScript object-graph mapper for Neo4j, Aura and Memgraph, with an embedded Relate engine for running in-process. Models in, one Cypher statement per read out. The cell below replays a verified Neo4j run of the quickstart recipe.
import { Direction, NodeEntity, OneToMany, Property, Uuid, fromEnv } from 'neode'
@NodeEntity()
export class Person {
@Uuid({ primary: true })
id: string
@Property({ type: 'string', unique: true, required: true, min: 2, max: 50 })
name: string
@Property({ type: 'int' })
age: number
}
@NodeEntity()
export class Movie {
@Property({ type: 'string', unique: true, primary: true })
title: string
// direction 'in': the stored relationship is (Person)-[:ACTED_IN]->(Movie)
@OneToMany(() => Person, 'ACTED_IN', { direction: 'in', eager: true })
actors: Person[]
}
async function main(): Promise<void> {
const neode = fromEnv()
try {
// CREATE — validated against the model before the write
const ray = await neode.create(Person, { name: 'Ray Liotta', age: 40 })
// READ — by primary key
const found = await neode.find(Person, ray.id)
// UPDATE, then relate: Ray Liotta ACTED_IN Goodfellas — the relationship
// key is declared on the Movie model, so the relate runs from that side
await neode.update(found, { age: 41 })
const goodfellas = await neode.create(Movie, { title: 'Goodfellas' })
await neode.relate(Movie, goodfellas, found, 'actors')
// One eager read — the movie and its cast, hydrated in one statement
const movie = await neode.find(Movie, goodfellas.title, { with: { actors: true } })
console.log(`${movie?.title}: ${movie?.actors.length} actor(s) — ${movie?.actors[0]?.name}`)
} finally {
await neode.close()
}
}
main().catch(console.error)
QUERY_EXECUTED · captured from the verified build run
// Run to see the Cypher neode generated
Same code. Bring your own engine.
Neo4j Community, Enterprise or Aura. Or Relate, embedded in-process with
Neode.forRelate() and compiled to WebAssembly, which is why the
playground needs no server.
Neo4j
Community · Enterprise
Aura
managed Neo4j
Relate
embedded · WASM
The API, area by area.
:Modeling Model definition
- api
- TC39 decorators | defineModel()
:Reads CRUD and reads
:Ops Schema and operations
:Search Search, vectors and agents
Decorators or a plain map. Same metamodel.
Hover a property in either style and find it on the schema card. The map API is also the migration path from 0.x.
decorators
import { NodeEntity, Property, Uuid } from 'neode' @NodeEntity() export class Person { @Uuid({ primary: true }) id: string @Property({ type: 'string', unique: true, required: true, min: 2, max: 50 }) name: string @Property({ type: 'int' }) age: number }
map-based
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 })
one metamodel · two ways to say it
One query per read.
Eager loading and read-model decorators compile into a single Cypher statement. No N+1, however deep the hydration.
movie.ts
@NodeEntity()
export class Movie {
@Property({ type: 'string', unique: true, primary: true })
title: string
@OneToMany(() => Person, 'ACTED_IN', { direction: 'out', eager: true })
actors: Person[]
}
const movie = await neode.find(Movie, title, { with: { actors: true } })
// one Cypher statement, fully hydratedHonest caveats.
1.0 is an alpha
Install with @next. A plain install still gives 0.4.9.
Node.js 20+
Required.
Models are process-global
One schema per process.
exists constraints
Need Neo4j Enterprise.
Coming from 0.4.9? Map-based schemas keep working. Read the migration guide.
From install to first query in four steps.
- 1Installnpm install neode@next
- 2ConnectNeo4j, Aura, or Neode.forRelate(database)
- 3Define a model@NodeEntity() or defineModel()
- 4Create, read, update, deleteneode.create · find · update · delete