Build and configure a GraphQL API backed by Neo4j using @neo4j/graphql v7 (current) or v5 (LTS).
---
name: neo4j-graphql-skill
description: Build and configure a GraphQL API backed by Neo4j using @neo4j/graphql v7 (current) or v5 (LTS).
Covers Neo4jGraphQL constructor, getSchema(), assertIndexesAndConstraints(), type definitions with @node,
@relationship (IN/OUT/UNDIRECTED), @cypher for custom resolvers, @authorization/@authentication for JWT/JWKS
security, auto-generated queries/mutations, OGM programmatic access, subscriptions via CDC, and Apollo
Federation. Use when writing typeDefs, securing fields, or wiring Neo4j to Apollo Server.
Does NOT handle raw Cypher outside resolvers — use neo4j-cypher-skill.
Does NOT cover Spring Data Neo4j entity mapping — use neo4j-spring-data-skill.
version: 1.0.1
allowed-tools: Bash WebFetch
---
## When to Use
- Creating a GraphQL API from a Neo4j graph schema with `@neo4j/graphql`
- Writing type definitions with `@relationship`, `@cypher`, `@authorization` directives
- Using OGM for server-side programmatic Neo4j access (bypasses GraphQL auth)
- Configuring auto-generated queries, mutations, subscriptions
- Securing types/fields with JWT or JWKS-based `@authorization` rules
- Migrating from v5/v6 to v7 (breaking changes below)
## When NOT to Use
- **Raw Cypher queries outside GraphQL resolvers** → `neo4j-cypher-skill`
- **Spring Data Neo4j / Java entity mapping** → `neo4j-spring-data-skill`
- **Generic GraphQL without Neo4j** — outside scope
---
## Version Matrix
| Version | Status | Notes |
|---|---|---|
| v7 | Current | `@node` required; `options` removed; explicit `eq` syntax |
| v5 | LTS | Older syntax; `options: {limit, offset, sort}` still valid |
Default to v7 unless codebase is on v5.
---
## Step 1 — Install
```bash
npm install @neo4j/graphql neo4j-driver graphql @apollo/server
```
For subscriptions (CDC required):
```bash
npm install ws graphql-ws express body-parser cors
```
---
## Step 2 — Minimal Server Setup
```javascript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { Neo4jGraphQL } from '@neo4j/graphql';
import neo4j from 'neo4j-driver';
const typeDefs = `#graphql
type Movie @node {
id: ID! @id
title: String!
actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN)
}
type Person @node {
id: ID! @id
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
}
`;
const driver = neo4j.driver(
process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USERNAME, process.env.NEO4J_PASSWORD)
);
const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
// assertIndexesAndConstraints syncs @id → UNIQUE constraints; wrap in try/catch
await neoSchema.assertIndexesAndConstraints({ options: { create: true } });
const server = new ApolloServer({ schema: await neoSchema.getSchema() });
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({ token: req.headers.authorization }),
listen: { port: 4000 },
});
```
`assertIndexesAndConstraints` throws if constraints missing. Use `{ create: true }` to auto-create, or run `CREATE CONSTRAINT` manually and retry.
---
## Key Directives
### @node (v7 required)
Every GraphQL type representing a Neo4j node must have `@node`. Without it, v7 ignores the type.
```graphql
type Product @node {
id: ID! @id
name: String!
}
# Custom label (default = type name)
type Article @node(labels: ["Post", "Content"]) {
title: String!
}
```
### @relationship — Full Syntax
```graphql
type Person @node {
# direction: OUT = (this)-[:KNOWS]->(other)
friends: [Person!]! @relationship(type: "KNOWS", direction: OUT)
# direction: IN = (other)-[:ACTED_IN]->(this)
actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: IN)
# direction: UNDIRECTED = matches both directions (use sparingly — double-counts)
colleagues: [Person!]! @relationship(type: "COLLEAGUE_OF", direction: UNDIRECTED)
# Relationship with properties — reference an @relationshipProperties interface
reviews: [Movie!]! @relationship(type: "REVIEWED", direction: OUT, properties: "ReviewedProps")
}
interface ReviewedProps @relationshipProperties {
rating: Int!
date: Date
}
```
Direction rule: `OUT` = arrow leaves this node. `IN` = arrow enters this node. Both sides of a relationship must declare opposite directions.
### Querying Relationship Properties — Connection API
For each relationship with `properties:`, a `{field}Connection` field is auto-generated. Access rel properties via `actorsConnection.edges.properties`, not via `actors`:
```graphql
query {
movies(where: { title: { eq: "The Matrix" } }) {
title
actorsConnection {
edges {
properties { role } # maps to @relationshipProperties interface
node { name }
}
}
}
}
```
### @cypher — Custom Resolver
```graphql
type Person @node {
name: String!
# columnName must exactly match the RETURN alias — mismatch returns null silently
friendCount: Int
@cypher(
statement: "MATCH (this)-[:KNOWS]->(f:Person) RETURN count(f) AS friendCount"
columnName: "friendCount"
)
recommendedMovies: [Movie!]!
@cypher(
statement: """
MATCH (this)-[:WATCHED]->(m:Movie)<-[:WATCHED]-(o:Person)-[:WATCHED]->(rec:Movie)
WHERE NOT (this)-[:WATCHED]->(rec)
RETURN rec
"""
columnName: "rec"
)
}
# @cypher on Query field — custom top-level query
type Query {
topRatedMovies(limit: Int = 10): [Movie!]!
@cypher(
statement: "MATCH (m:Movie) WHERE m.rating IS NOT NULL RETURN m ORDER BY m.rating DESC LIMIT $limit"
columnName: "m"
)
}
```
`this` refers to the current node in field-level @cypher. Parameters are passed as `$paramName`.
### @cypher — Field Arguments and extend type
```graphql
# extend type adds computed fields without modifying the base type definition
extend type Movie @node {
avgRating: Float
@cypher(statement: "MATCH (this)<-[r:RATED]-(:User) RETURN avg(r.rating) AS result", columnName: "result")
# Field arguments passed as Cypher params; always provide default to avoid null
recommended(limit: Int = 3): [Movie!]!
@cypher(
statement: "MATCH (this)<-[:RATED]-(u:User)-[:RATED]->(rec:Movie) WITH rec, COUNT(u) AS score ORDER BY score DESC RETURN rec LIMIT $limit"
columnName: "rec"
)
}
```
### @id and @timestamp
```graphql
type Post @node {
id: ID! @id # auto-generates UUID; creates UNIQUE constraint
createdAt: DateTime! @timestamp(operations: [CREATE])
updatedAt: DateTime @timestamp(operations: [CREATE, UPDATE])
title: String!
}
```
### @alias — Map GraphQL field to Neo4j property
```graphql
type User @node {
id: ID! @id
email: String! @alias(property: "emailAddress") # GraphQL: email → DB: emailAddress
}
```
---
## Security — @authentication and @authorization
### Step 1: Configure JWT in constructor
```javascript
// Symmetric secret
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: {
authorization: { key: process.env.JWT_SECRET },
},
});
// JWKS endpoint (production)
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: {
authorization: {
key: { url: 'https://myapp.com/.well-known/jwks.json' },
},
},
});
```
### Step 2: Pass token in context
```javascript
context: async ({ req }) => ({ token: req.headers.authorization }),
// Or pass pre-decoded JWT:
context: async ({ req }) => ({ jwt: myDecodeJwt(req.headers.authorization) }),
```
### Step 3: Apply @authentication and @authorization
```graphql
# Require auth on all operations for a type
type Post @node
@authentication
@authorization(filter: [{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }]) {
title: String!
author: User! @relationship(type: "AUTHORED", direction: IN)
}
# requireAuthentication: false = allow public access without JWT
type Article @node
@authorization(filter: [
{ requireAuthentication: false, where: { node: { published: { eq: true } } } }
{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }
]) {
title: String!
published: Boolean!
}
# validate (throws error) vs filter (silently hides data)
type BankAccount @node
@authorization(validate: [{
when: [BEFORE],
where: { node: { owner: { id: { eq: "$jwt.sub" } } } }
}]) {
balance: Float!
}
# Role-based with custom JWT claims
type JWT @jwt {
roles: [String!]! @jwtClaim(path: "myApp.roles")
}
type AdminReport @node
@authentication(operations: [READ], jwt: { roles: { includes: "admin" } }) {
data: String!
}
```
**filter vs validate**: `filter` silently removes unauthorized data. `validate` throws an error. Use `validate` when data existence should not be revealed to unauthorized users.
**BEFORE vs AFTER**: `CREATE` supports only `AFTER`; `READ` supports only `BEFORE`.
---
## Auto-Generated Operations
For each `@node` type, the library generates:
| Operation | Generated Name | Example |
|---|---|---|
| Query all | `{plural}` | `movies(where, sort, limit, offset)` |
| Cursor pagination | `{plural}Connection` | `moviesConnection(first, after, where, sort)` |
| Create | `create{Plural}` | `createMovies(input: [MovieCreateInput!]!)` |
| Update | `update{Plural}` | `updateMovies(where, update)` |
| Delete | `delete{Plural}` | `deleteMovies(where, delete)` |
### v7 Filter Syntax (explicit `eq`)
```graphql
# v7: explicit eq required
query {
movies(where: { title: { eq: "The Matrix" } }) {
title
actors { name }
}
}
# Sort and paginate (v7: direct args, not options wrapper)
query {
movies(sort: [{ title: ASC }], limit: 10, offset: 0) {
title
}
}
```
### Nested Mutations
```graphql
mutation {
createMovies(input: [{
title: "Inception"
actors: {
create: [{ node: { name: "Leonardo DiCaprio" } }]
connect: { where: { node: { name: { eq: "Joseph Gordon-Levitt" } } } }
}
}]) {
movies { id title }
}
}
```
`connectOrCreate` was removed in v7. Use `connect` + `create` separately.
---
## OGM — Programmatic Access
OGM bypasses GraphQL authorization — use only in trusted server-side contexts.
```javascript
import { OGM } from '@neo4j/graphql-ogm';
const ogm = new OGM({ typeDefs, driver });
await ogm.init(); // must await before using models
const Movie = ogm.model('Movie');
// find
const movies = await Movie.find({
where: { title: { eq: 'The Matrix' } },
selectionSet: `{ id title actors { name } }`,
});
// create
const { movies: created } = await Movie.create({
input: [{ title: 'Dune', actors: { create: [{ node: { name: 'Timothée Chalamet' } }] } }],
});
// update
await Movie.update({
where: { id: { eq: movieId } },
update: { title: { set: 'Dune: Part One' } },
});
// delete
await Movie.delete({ where: { id: { eq: movieId } } });
```
Install separately: `npm install @neo4j/graphql-ogm`
---
## Subscriptions (CDC Required)
Requires Neo4j CDC enabled in `FULL` mode. See [CDC docs](https://neo4j.com/docs/cdc/current/).
```javascript
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: { subscriptions: true },
});
```
Subscriptions auto-generate for each type:
```graphql
subscription {
movieCreated(where: { title: { eq: "The Matrix" } }) {
createdMovie { title }
}
}
# Also: movieUpdated, movieDeleted
```
---
## Schema Control Directives
```graphql
type ReadOnlyData @node @mutation(operations: []) { value: String! } # disable mutations
type HeavyDoc @node {
id: ID! @id
content: String! @filterable(byValue: false) @sortable(enabled: false) # perf guard
title: String!
}
type Series @node @plural(value: "seriesList") { title: String! } # irregular plural fix
```
---
## Common Errors
| Error | Cause | Fix |
|---|---|---|
| `Type 'X' not found` | Missing `@node` on type (v7) | Add `@node` to every node type |
| `@cypher` field returns null | `columnName` mismatch with RETURN alias | Match `columnName` exactly to RETURN alias |
| Relationship direction mismatch | Both sides declare same direction | Inverse: if A has `direction: OUT`, B must have `direction: IN` |
| `assertIndexesAndConstraints` throws | `@id` constraint not in DB | Add `{ options: { create: true } }` or run `CREATE CONSTRAINT` manually |
| Auth not applied | JWT not in context | Pass `token: req.headers.authorization` in context function |
| 0 results with valid data | v7 filter missing `eq` | Use `{ field: { eq: value } }` not `{ field: value }` |
| `connectOrCreate not found` | Removed in v7 | Use `connect` + `create` separately |
| Memory errors on large mutations | Complex Cypher generation | Batch mutations; increase `server.memory.heap.max_size` |
| `@subscription` not generating | v7 requires explicit enable | Add `features: { subscriptions: true }` to constructor |
---
## v6 → v7 Breaking Changes Summary
| v6 | v7 |
|---|---|
| `@node` optional | `@node` required on every node type |
| `options: { limit, sort }` | `limit`, `sort` as direct args |
| `{ field: value }` filter | `{ field: { eq: value } }` |
| `connectOrCreate` nested mutation | Removed — use `connect` + `create` |
| `directed` arg on queries | `queryDirection` in `@relationship` |
| Single rel fields `actor: Person` | Must use list `actors: [Person!]!` |
| `@private` directive | Removed |
| `@unique` directive | Removed |
---
## References
- [Neo4j GraphQL Docs](https://neo4j.com/docs/graphql/current/) — full directive reference, migration guides
- [GraphAcademy: GraphQL Basics](https://graphacademy.neo4j.com/courses/graphql-basics/) — hands-on course
- [GitHub: @neo4j/graphql](https://github.com/neo4j/graphql) — changelog, issues
- [CDC Setup](https://neo4j.com/docs/cdc/current/) — required for subscriptions
---
## Checklist
- [ ] `@node` on every GraphQL type representing a Neo4j node (v7 hard requirement)
- [ ] `@id` on identity fields (triggers `CREATE CONSTRAINT` via `assertIndexesAndConstraints`)
- [ ] `assertIndexesAndConstraints` called on startup with try/catch
- [ ] `@relationship` direction correct: `OUT` = arrow leaves this node, `IN` = arrow enters
- [ ] Both sides of relationship declared with inverse directions
- [ ] `@cypher` `columnName` matches RETURN alias exactly
- [ ] JWT secret or JWKS URL in `features.authorization.key`; token passed in context
- [ ] `@authorization` filter vs validate chosen deliberately (silent hide vs thrown error)
- [ ] v7: filters use explicit `{ field: { eq: value } }` syntax
- [ ] v7: `limit`/`sort` passed as direct query args (not `options` wrapper)
- [ ] OGM: `await ogm.init()` called before any `ogm.model()` usage
- [ ] Subscriptions: CDC enabled in FULL mode before enabling `features.subscriptions`
- [ ] `.env` holds credentials; `.env` in `.gitignore`
Creator's repository · neo4j-contrib/neo4j-skills