neo4j-graphql-skill — Claude Code skill: GraphQL API on Neo4j
A skill that walks your agent through building a GraphQL API on @neo4j/graphql: type definitions, directives, JWT auth and Apollo Server wiring.
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphql-skill -a claude-codeWhat neo4j-graphql-skill does
The skill covers @neo4j/graphql end to end, from installing packages to starting a server. It documents the Neo4jGraphQL constructor, getSchema(), assertIndexesAndConstraints(), type definitions with @node and @relationship (IN, OUT, UNDIRECTED), custom resolvers via @cypher, and @authorization / @authentication for JWT and JWKS.
A version matrix separates v7 (current) from v5 (LTS) and lists the minimum patch for each. Those patches close real holes. One is an auth bypass where subscriptions accepted an unverified JWT from WebSocket connectionParams; another is a privilege escalation where field-level @authentication was ignored when the operation type also carried type-level @authentication. If your API already ships auth, check your package version before the next release.
Auto-generated queries and mutations, OGM access, CDC subscriptions and Apollo Federation are all in scope. Raw Cypher outside resolvers is not — that belongs to neo4j-cypher-skill, and Spring Data Neo4j entity mapping to neo4j-spring-data-skill. Subscriptions pull in ws, graphql-ws, express, body-parser and cors, and need CDC enabled on the Neo4j side.
What it helps with
- turn a Neo4j graph model into a GraphQL schema
- declare relationships with the right direction via
@relationship - write a
@cypherresolver without breaking thecolumnNamealias - lock down fields and types with
@authorizationrules - stand up Apollo Server on top of
Neo4jGraphQL
Example prompts for the agent
- generate typeDefs for movies and actors with
@nodeand@relationship - add a
friendCountfield with@cypherand a matchingcolumnName - add an
@authorizationrule on theemailfield using the JWT header
How to install neo4j-graphql-skill
1Skills CLI (Vercel)
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphql-skill -a claude-code2Manually: put the files in place
~/.claude/skills/neo4j-graphql-skill/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/neo4j-graphql-skill/
1Skills CLI (Vercel)
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphql-skill -a codex2Manually: put the files in place
.agents/skills/neo4j-graphql-skill/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphql-skill -a cursor2Manually: put the files in place
.cursor/skills/neo4j-graphql-skill/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install neo4j-contrib/neo4j-skills neo4j-graphql-skill2Manually: put the files in place
.github/skills/neo4j-graphql-skill/SKILL.md1Skills CLI (Vercel)
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphql-skill -a gemini-cli2Platform CLI
gemini skills install https://github.com/neo4j-contrib/neo4j-skills --path neo4j-graphql-skillManually: put the files in place
.devin/skills/neo4j-graphql-skill/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphql-skill -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about neo4j-graphql-skill
How do I install neo4j-graphql-skill in Claude?
Run npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphql-skill -a claude-code. The skill lands at ~/.claude/skills/neo4j-graphql-skill/SKILL.md.
What does neo4j-graphql-skill do?
It helps you write and configure a GraphQL API backed by Neo4j: type definitions with directives, @cypher resolvers, authorization rules and Apollo Server setup.
How is it different from neo4j-cypher-skill?
It does not handle raw Cypher outside GraphQL resolvers — that is neo4j-cypher-skill. Spring Data Neo4j entity mapping is also out of scope.
Source text
SKILL.md · MIT
---
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.5
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 — use ≥ 7.6.2 | `@node` required; `options` removed; explicit `eq` syntax |
| v5 | LTS — use ≥ 5.12.15 | Older syntax; `options: {limit, offset, sort}` still valid |
Default to v7 unless codebase is on v5. Upgrade past these security patches before shipping auth:
| Fix | Fixed in |
|---|---|
| Subscriptions accepted an unverified JWT from WebSocket `connectionParams` (auth/authz bypass) | 7.5.6 / 5.12.14 |
| Field-level `@authentication` on a root custom-resolver field ignored when the operation type also had type-level `@authentication` (privilege escalation) | 7.6.0 / 5.12.15 |
| `@authorization` rules not applied on create-relationship operations | 7.6.2 |
---
## 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 },
});
```
---
## Key Directives
### @node (v7 required)
```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
}
```
### 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
}
```
### @fulltext and @vector
Index must exist in Cypher before use. Phrase-capable `@vector` indexes accept `maxPhraseLength` [7.6.0] to cap embedding cost. A `provider` without matching `features.vector` config fails at schema build [7.6.1]. Full syntax, generated query shapes, provider config: [references/search-directives.md](references/search-directives.md).
---
## 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!
}
```
**BEFORE vs AFTER**: `CREATE` supports only `AFTER`; `READ` supports only `BEFORE`.
---
## Auto-Generated Operations
| 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 }
}
}
```
---
## 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 },
});
```
Authenticate subscriptions with a verified `token` in the WebSocket context; a pre-decoded `jwt` is trusted only when set server-side (7.5.6 / 5.12.14 stopped trusting client-supplied `connectionParams` JWTs).
```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
- [references/search-directives.md](references/search-directives.md) — `@fulltext`, `@vector`, `maxPhraseLength`
---
## 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`
- [ ] `@neo4j/graphql` ≥ 7.6.2 (v7) or ≥ 5.12.15 (LTS) — earlier versions have auth bypasses
- [ ] `.env` holds credentials; `.env` in `.gitignore`
© neo4j-contrib · MIT · neo4j-contrib/neo4j-skills