Skip to content

Commit f4ab1d1

Browse files
committed
converted graphql middleware blog post
1 parent b7b3453 commit f4ab1d1

File tree

1 file changed

+353
-0
lines changed

1 file changed

+353
-0
lines changed
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
= Using GraphQL middleware with Neo4j GraphQL
2+
3+
You can wrap your auto-generated Neo4j GraphQL resolver with custom logic that intercepts specific GraphQL operations.
4+
This approach allows you to retain all the benefits of auto-generation while adding the custom behavior you need.
5+
6+
7+
== GraphQL middleware
8+
9+
This page makes use of https://github.com/dimatill/graphql-middleware[`graphql-middleware`].
10+
GraphQL middleware is a library that provides a way to wrap and extend the behavior of your GraphQL resolvers.
11+
It acts as a layer that allows you to apply reusable logic, such as logging, validation, or authentication, across multiple resolvers in a consistent and modular
12+
way.
13+
14+
15+
=== Logging every request
16+
17+
Consider this Neo4j GraphQL setup:
18+
19+
[source,typescript]
20+
----
21+
import { ApolloServer } from "@apollo/server";
22+
import { startStandaloneServer } from "@apollo/server/standalone";
23+
import { applyMiddleware } from "graphql-middleware";
24+
import * as neo4j from "neo4j-driver";
25+
import { Neo4jGraphQL } from "@neo4j/graphql";
26+
27+
const typeDefs = /* GraphQL */ `
28+
type User @node {
29+
id: ID! @id
30+
name: String!
31+
email: String!
32+
posts: [Post!]! @relationship(type: "AUTHORED", direction: OUT)
33+
}
34+
35+
type Post @node {
36+
id: ID!
37+
title: String!
38+
content: String!
39+
author: [User!]! @relationship(type: "AUTHORED", direction: IN)
40+
}
41+
42+
type Query {
43+
me: User @cypher(statement: "MATCH (u:User {id: $userId}) RETURN u", columnName: "u")
44+
}
45+
`;
46+
47+
const driver = neo4j.driver("bolt://localhost:7687", neo4j.auth.basic("neo4j", "password"));
48+
49+
const neoSchema = new Neo4jGraphQL({
50+
typeDefs,
51+
driver,
52+
});
53+
54+
const server = new ApolloServer({
55+
schema: await neoSchema.getSchema(),
56+
});
57+
58+
const { url } = await startStandaloneServer(server, {
59+
listen: { port: 4000 },
60+
});
61+
console.log(`🚀 Server ready at ${url}`);
62+
----
63+
64+
Add logging to every single operation without touching the generated schema:
65+
66+
[source,typescript]
67+
----
68+
import { applyMiddleware } from "graphql-middleware";
69+
70+
/* ...existing code... */
71+
72+
const logMiddleware = async (resolve, root, args, context, info) => {
73+
const start = Date.now();
74+
console.log(`🚀 ${info.fieldName} started`);
75+
76+
try {
77+
const result = await resolve(root, args, context, info);
78+
console.log(`✅ ${info.fieldName} completed in ${Date.now() - start}ms`);
79+
return result;
80+
} catch (error) {
81+
console.log(`💥 ${info.fieldName} failed`);
82+
throw error;
83+
}
84+
};
85+
86+
// Wrap your executable schema
87+
const schemaWithLogging = applyMiddleware(await neoSchema.getSchema(), {
88+
Query: logMiddleware,
89+
Mutation: logMiddleware,
90+
});
91+
92+
const server = new ApolloServer({ schema: schemaWithLogging });
93+
----
94+
95+
*That’s it. Every query and mutation is now logged.* Your auto-generated
96+
resolver is unchanged, but you’ve added custom behavior.
97+
98+
Query the users:
99+
100+
[source,graphql]
101+
----
102+
{
103+
users {
104+
name
105+
}
106+
}
107+
----
108+
109+
You should see in your server:
110+
111+
....
112+
🚀 users started
113+
✅ users completed in 23ms
114+
....
115+
116+
117+
=== Email validation before database writes
118+
119+
You can use middleware to enforce specific business rules before data is written to the database.
120+
For example, you can ensure that email addresses provided during user creation are valid.
121+
By using middleware, you can intercept and validate the input before it reaches the Neo4j GraphQL resolver.
122+
123+
Add a middleware that validates the email input in the `createUsers` operation.
124+
A validation error will be thrown before it reaches the Neo4j GraphQL resolver, and the GraphQL client will receive the error message "Invalid email addresses detected".
125+
126+
[source,typescript]
127+
----
128+
/* ...existing code... */
129+
130+
const validateEmails = async (resolve, root, args, context, info) => {
131+
// Only check createUsers mutations
132+
if (info.fieldName === "createUsers") {
133+
// Note: This is a simplistic and intentionally flawed email validation example, but good for demonstration purposes.
134+
const invalidEmails = args.input.filter((user) => !user.email.includes("@"));
135+
if (invalidEmails.length > 0) {
136+
throw new Error("Invalid email addresses detected");
137+
}
138+
}
139+
140+
return resolve(root, args, context, info);
141+
};
142+
143+
const schema = applyMiddleware(
144+
await neoSchema.getSchema(),
145+
{
146+
Query: logMiddleware,
147+
Mutation: logMiddleware,
148+
},
149+
{
150+
Mutation: validateEmails,
151+
}
152+
);
153+
----
154+
155+
Try to create a user with the email "not-an-email":
156+
157+
[source,graphql]
158+
----
159+
mutation createUsers {
160+
createUsers(input: [{ email: "not-an-email.com", name: "firstname" }]) {
161+
users {
162+
email
163+
}
164+
}
165+
}
166+
----
167+
168+
169+
== Working with Neo4j GraphQL
170+
171+
Most of the above is applicable even outside Neo4j GraphQL, but there is an important concept when writing middleware for Neo4j GraphQL resolvers.
172+
173+
Here's the key difference from how traditional GraphQL resolvers are
174+
usually built:
175+
176+
In *traditional GraphQL*, each field resolver executes independently, potentially causing multiple database calls.
177+
By contrast, in *Neo4j GraphQL* the root field resolver (like `users` or `createUsers`) analyzes the entire query tree and executes one optimized Cypher query.
178+
179+
The N+1 problem is solved in Neo4j GraphQL by analyzing the entire GraphQL operation (via the `info` object) and generating optimized Cypher queries that fetch all requested data in a single database round-trip.
180+
181+
Consider this query:
182+
183+
[source,graphql]
184+
----
185+
{
186+
users {
187+
name
188+
email
189+
posts {
190+
title
191+
content
192+
}
193+
}
194+
}
195+
----
196+
197+
Neo4j GraphQL doesn't execute separate resolvers for `name`, `email`, `posts`, `title`, and `content`.
198+
Instead, the `users` field resolver generates and executes a single Cypher query that returns all the data at once.
199+
The nested field resolvers simply return the already fetched data from memory.
200+
201+
202+
=== Timing matters
203+
204+
Timing matters for middleware - by the time the individual field resolvers execute, the database has already been queried and the data is available in the resolver's result.
205+
206+
Consider the `logMiddleware` from above:
207+
208+
[source,typescript]
209+
----
210+
const logMiddleware = async (resolve, root, args, context, info) => {
211+
const start = Date.now();
212+
console.log(`🚀 ${info.fieldName} started`);
213+
214+
try {
215+
const result = await resolve(root, args, context, info);
216+
console.log(`✅ ${info.fieldName} completed in ${Date.now() - start}ms`);
217+
return result;
218+
} catch (error) {
219+
console.log(`💥 ${info.fieldName} failed: ${error.message}`);
220+
throw error;
221+
}
222+
};
223+
----
224+
225+
Apply the `logMiddleware` to queries and the user's name:
226+
227+
[source,typescript]
228+
----
229+
const schema = applyMiddleware(
230+
schema,
231+
{
232+
Query: logMiddleware, // wraps all the Queries and it's executed before the database round-trip
233+
},
234+
{
235+
User: {
236+
name: logMiddleware, // wraps only the User's name field resolver and it's executed after the database roundtrip
237+
},
238+
}
239+
);
240+
----
241+
242+
Run this query:
243+
244+
[source,graphql]
245+
----
246+
query {
247+
users {
248+
name
249+
}
250+
}
251+
----
252+
253+
You should see:
254+
255+
....
256+
🚀 users started
257+
... Neo4j resolver generates and executes Cypher ...
258+
✅ users completed in 48ms
259+
🚀 name started
260+
✅ name completed in 0ms
261+
....
262+
263+
Note how the name resolution happens after the round-trip to the database.
264+
265+
Note the following difference:
266+
267+
* Query and mutation level middleware runs before and after the Neo4j GraphQL autogenerated resolvers.
268+
* Type and field level middleware runs only after the Neo4j GraphQL autogenerated resolvers.
269+
270+
271+
== Stack multiple middleware
272+
273+
It's possible to apply multiple pieces of middleware for the same field.
274+
For instance, you can apply diverse middleware to the same `users` resolver:
275+
276+
[source,typescript]
277+
----
278+
const schema = applyMiddleware(
279+
schema,
280+
{
281+
Query: {
282+
users: async (resolve, root, args, context, info) => {
283+
console.log("A started");
284+
await resolve(root, args, context, info);
285+
console.log("A completed");
286+
},
287+
},
288+
},
289+
{
290+
Query: {
291+
users: async (resolve, root, args, context, info) => {
292+
console.log("B started");
293+
await resolve(root, args, context, info);
294+
console.log("B completed");
295+
},
296+
},
297+
},
298+
{
299+
Query: {
300+
users: async (resolve, root, args, context, info) => {
301+
console.log("C started");
302+
await resolve(root, args, context, info);
303+
console.log("C completed");
304+
},
305+
},
306+
}
307+
);
308+
----
309+
310+
The order in which middleware is applied is important, as they execute one after the other.
311+
Each middleware wraps the next one, creating a chain of execution from outermost to innermost.
312+
313+
Run this query:
314+
315+
[source,graphql]
316+
----
317+
query {
318+
users {
319+
name
320+
}
321+
}
322+
----
323+
324+
Schematic output:
325+
326+
[source,bash]
327+
----
328+
....
329+
A started
330+
B started
331+
C started
332+
... Neo4j GraphQL user resolver ...
333+
C completed
334+
B completed
335+
A completed
336+
....
337+
----
338+
339+
The user's resolver is wrapped in three layers of middleware.
340+
341+
342+
== Conclusion
343+
344+
GraphQL middleware with Neo4j GraphQL gives you the best of both worlds: the power of auto-generated schemas and the flexibility to inject custom logic exactly where you need it.
345+
346+
When you need custom logic, graphql-middleware lets you keep the rapid development benefits of Neo4j GraphQL while adding the custom behavior you need.
347+
348+
The GraphQL ecosystem evolves rapidly.
349+
https://the-guild.dev/[The Guild] has developed https://envelop.dev/[Envelop] with its own https://www.npmjs.com/package/@envelop/graphql-middleware[graphql-middleware
350+
plugin].
351+
352+
This guide uses `graphql-middleware` because it's server-agnostic and delivers the clearest path to understanding middleware with Neo4j GraphQL.
353+
If you need a more comprehensive plugin ecosystem, we recommend exploring envelop.

0 commit comments

Comments
 (0)