Skip to main content
skill CX Codex

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.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphql-skill -a claude-code
Open on GitHub

What 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 @cypher resolver without breaking the columnName alias
  • lock down fields and types with @authorization rules
  • stand up Apollo Server on top of Neo4jGraphQL

Example prompts for the agent

  • generate typeDefs for movies and actors with @node and @relationship
  • add a friendCount field with @cypher and a matching columnName
  • add an @authorization rule on the email field 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-code

2Manually: put the files in place

~/.claude/skills/neo4j-graphql-skill/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/neo4j-graphql-skill/

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

Similar skills

Everything for Backend and APIs →

From the same set18

neo4j-getting-started-skill Walks you from an empty folder to a running Neo4j app in eight stages, from tool checks to a notebook or service. CX Codex +1 888installs neo4j-gds-skill Guides graph projections, algorithms, and pipelines for the embedded Neo4j GDS plugin. Useful for Python and Cypher graph analytics workflows. CX Codex +1 865installs neo4j-security-skill This skill walks an agent through Neo4j user, role and privilege management in Cypher against the system database. Useful when access has to be provisioned by script instead of by hand. CX Codex +1 824installs neo4j-genai-plugin-skill A skill for calling Neo4j GenAI Plugin functions and procedures inside Cypher: embeddings, completion, chat, tokenization. CX Codex +1 812installs neo4j-aura-graph-analytics-skill A skill that walks your agent through serverless Aura Graph Analytics GDS Sessions: graph projection, memory estimation, async jobs and write-back. CX Codex +1 798installs neo4j-aura-provisioning-skill Creates and manages Neo4j Aura instances through aura-cli or the REST API: pause, resume, resize, delete, credential download. CX Codex +1 789installs neo4j-kafka-skill Sets up the Neo4j Connector for Kafka in both directions and the native CDC API. Useful when Kafka events have to land in a graph or Neo4j changes need to reach topics. CX Codex +1 772installs neo4j-driver-java-skill Tells your agent how to wire Java or Kotlin into Neo4j: driver setup, sessions, transactions, connection pool. CX Codex +1 770installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor