Decorator-Based Models
Define Neo4j models using TypeScript decorators with full type inference
Decorator-based models
Define models using decorators instead of separate type declarations and configuration objects.
Overview
A model is defined as a class with four decorators:
@Node-- marks the class as a Neo4j node and sets the label@PrimaryKey-- marks a field as the primary key (also registers it as a property)@Property-- marks a field as a stored property, with optional TypeBox validation@Relationship-- declares a relationship to another model
import {
Node,
PrimaryKey,
Property,
Relationship,
NodeEntity,
Type,
} from 'neogma';
import type { Related } from 'neogma';
@Node({ label: 'User' })
class UserNode extends NodeEntity {
@PrimaryKey(Type.String())
id!: string;
@Property(Type.String({ minLength: 3 }))
name!: string;
@Property(Type.Optional(Type.Number({ minimum: 0 })))
age?: number;
@Relationship({
name: 'PLACED',
direction: 'out',
model: () => OrderNode,
})
Orders!: Related<typeof OrderNode>;
}
// Register the model with neogma
const Users = neogma.model(UserNode);The @Node decorator
@Node marks a class as a Neo4j node. It accepts:
| Option | Type | Description |
|---|---|---|
label | string | string[] | The Neo4j label (or labels) for this node. Defaults to the class name |
// Explicit label
@Node({ label: 'User' })
// Multiple labels
@Node({ label: ['User', 'Person'] })
// Label defaults to the class name ('UserNode')
@Node()The class must extend NodeEntity:
import { NodeEntity } from 'neogma';
@Node({ label: 'User' })
class UserNode extends NodeEntity {
// ...
}The @PrimaryKey decorator
@PrimaryKey marks a field as the primary key for the model. It also registers the field as a property, so you do not need to add @Property separately.
// With validation schema
@PrimaryKey(Type.String())
id!: string;
// With stricter validation
@PrimaryKey(Type.String({ format: 'uuid' }))
id!: string;
// Without validation (tracked but not validated)
@PrimaryKey()
id!: string;Each model must have exactly one @PrimaryKey field. The primary key is used for lookups and relationship matching.
The @Property decorator
@Property marks a field as a Neo4j property. It optionally takes a TypeBox schema for validation:
// With validation schema
@Property(Type.String({ minLength: 3 }))
name!: string;
// Optional property
@Property(Type.Optional(Type.Number()))
age?: number;
// Without validation (tracked but not validated)
@Property()
externalId!: string;Use the definite assignment operator (!) for required properties, not
declare. The declare keyword cannot be used with TC39 decorators.
Supported property types
Neogma supports all types that Neo4j can store natively:
stringnumberbooleanstring[],number[],boolean[]
Use TypeBox to define the validation:
import { Type } from 'neogma';
@Property(Type.String()) // string
@Property(Type.Number()) // number
@Property(Type.Boolean()) // boolean
@Property(Type.Array(Type.String())) // string[]
@Property(Type.Optional(Type.String())) // string | undefinedThe @Relationship decorator
@Relationship declares a relationship between two nodes:
@Relationship({
name: 'PLACED', // Neo4j relationship type
direction: 'out', // 'in', 'out', or 'none'
model: () => OrderNode, // lazy reference to the target model
})
Orders!: Related<typeof OrderNode>;Relationship options
| Option | Type | Description |
|---|---|---|
name | string | The Neo4j relationship type (e.g., 'PLACED') |
direction | 'in' | 'out' | 'none' | The relationship direction |
model | () => NodeEntityClass | 'self' | Lazy reference to the target model class, or 'self' for self-relationships |
properties | object | array | Optional relationship property definitions (see below) |
Relationship properties
Relationships can have their own properties. Use defineRelationshipProperties() to define the property config once and share it between @Relationship and Related<>:
import { defineRelationshipProperties, Type } from 'neogma';
const ratedProductRelProps = defineRelationshipProperties({
Rating: { property: 'rating', schema: Type.Number({ minimum: 1, maximum: 5 }) },
Review: { property: 'review', schema: Type.Optional(Type.String()) },
});
@Relationship({
name: 'RATED',
direction: 'out',
model: () => ProductNode,
properties: ratedProductRelProps,
})
RatedProducts!: Related<typeof ProductNode, typeof ratedProductRelProps>;Each entry maps an alias (the key you use when creating relationships, e.g. Rating) to a property (the name stored in Neo4j, e.g. rating) with a TypeBox schema for validation.
Co-locating properties as static class members
You can define relationship properties as a static member of the class itself using the function form for properties:
@Node({ label: 'User' })
class UserNode extends NodeEntity {
@PrimaryKey(Type.String()) id!: string;
static readonly orderRelProps = defineRelationshipProperties({
Rating: {
property: 'rating',
schema: Type.Number({ minimum: 1, maximum: 5 }),
},
});
@Relationship({
name: 'PLACED',
direction: 'out',
model: () => OrderNode,
properties: () => UserNode.orderRelProps,
})
Orders!: Related<typeof OrderNode, typeof UserNode.orderRelProps>;
}The () => UserNode.orderRelProps function form defers evaluation until neogma.model() runs, when static fields are guaranteed to be initialized.
Shorthand syntax
When the alias and Neo4j property name are the same, pass just the schema:
const relProps = defineRelationshipProperties({
rating: Type.Number({ minimum: 1, maximum: 5 }),
});Explicit type form
You can also specify both types manually:
RatedProducts!: Related<typeof ProductNode, { Rating: number }, { rating: number }>;Self-relationships
Use 'self' for relationships that point back to the same model:
@Node({ label: 'Person' })
class PersonNode extends NodeEntity {
@PrimaryKey(Type.String())
id!: string;
@Relationship({
name: 'KNOWS',
direction: 'out',
model: 'self',
})
Friends!: Related<typeof PersonNode>;
}Circular references
Relationships use lazy references (() => TargetClass) to handle circular dependencies. Neogma resolves them when models are registered:
@Node({ label: 'User' })
class UserNode extends NodeEntity {
@Relationship({ name: 'PLACED', direction: 'out', model: () => OrderNode })
Orders!: Related<typeof OrderNode>;
}
@Node({ label: 'Order' })
class OrderNode extends NodeEntity {
@Relationship({ name: 'PLACED', direction: 'in', model: () => UserNode })
User!: Related<typeof UserNode>;
}
// Register in any order
const Orders = neogma.model(OrderNode);
const Users = neogma.model(UserNode);The Related type
The Related type is used for relationship fields. It carries type information for the target model and optional relationship properties:
import type { Related } from 'neogma';
import { defineRelationshipProperties, Type } from 'neogma';
// No relationship properties
Orders!: Related<typeof OrderNode>;
// With relationship properties (recommended):
const relProps = defineRelationshipProperties({
Rating: { property: 'rating', schema: Type.Number() },
});
RatedProducts!: Related<typeof ProductNode, typeof relProps>;
// With explicit types:
RatedProducts!: Related<
typeof ProductNode,
{ Rating: number }, // properties used when creating the relationship
{ rating: number } // properties stored on the relationship
>;Registering models
After defining your model classes, register them with your neogma instance using neogma.model():
const neogma = new Neogma({/* connection */});
const Orders = neogma.model(OrderNode);
const Users = neogma.model(UserNode);The returned value is a fully typed NeogmaModel with all CRUD methods and relationship operations.
Adding custom static and instance methods
Define static and instance methods directly on your class:
@Node({ label: 'User' })
class UserNode extends NodeEntity {
@PrimaryKey(Type.String()) id!: string;
@Property(Type.String()) name!: string;
// Instance method
getDisplayName(): string {
return `User: ${this.name}`;
}
// Static method
static async findByName(this: typeof Users, name: string) {
return this.findOne({ where: { name } });
}
}
const Users = neogma.model(UserNode);Using legacy experimental decorators
If your project uses TypeScript's experimentalDecorators (common in NestJS projects), import from neogma/legacy:
import {
Node,
PrimaryKey,
Property,
Relationship,
NodeEntity,
} from 'neogma/legacy';
import { Type } from 'neogma';
import type { Related } from 'neogma';
@Node({ label: 'User' })
class UserNode extends NodeEntity {
@PrimaryKey(Type.String())
id!: string;
@Property(Type.String())
name!: string;
}The model definition syntax is identical. The only difference is the import path. Both produce the same NeogmaModel when registered.