Master modern API development with GraphQL. Learn schema definition, resolvers, mutations, subscriptions, and Apollo Server integration.
Master modern API development with GraphQL. Learn schema definition, resolvers, mutations, subscriptions, and Apollo Server integration.
Understand GraphQL fundamentals and benefits over REST
Design comprehensive GraphQL schemas and types
Implement resolvers for queries, mutations, and subscriptions
Integrate Apollo Server with Express.js
Handle data fetching and the N+1 problem
Build real-time applications with GraphQL subscriptions
GraphQL is a query language and runtime for APIs that enables clients to request exactly the data they need.
Learn about GraphQL vs REST Comparison.
1/*
2GraphQL Benefits over REST:
3
41. SINGLE ENDPOINT
5 - GraphQL: One endpoint (/graphql)
6 - REST: Multiple endpoints (/users, /posts, /comments)
7
82. PRECISE DATA FETCHING
9 - GraphQL: Request exactly what you need
10 - REST: Fixed data structure, often over/under-fetching
11
123. STRONG TYPE SYSTEM
13 - GraphQL: Schema-first development with type safety
14 - REST: Documentation-dependent, no built-in typing
15
164. REAL-TIME SUBSCRIPTIONS
17 - GraphQL: Built-in subscription support
18 - REST: Requires additional technologies (WebSockets, SSE)
19
205. INTROSPECTION
21 - GraphQL: Self-documenting with schema introspection
22 - REST: Requires external documentation
23*/
24
25// REST Example: Multiple requests needed
26const restApproach = async () => {
27 // 1. Get user
28 const user = await fetch('/api/users/123');
29
30 // 2. Get user's posts
31 const posts = await fetch(\`/api/users/\${user.id}/posts\`);
32
33 // 3. Get comments for each post
34 const postsWithComments = await Promise.all(
35 posts.map(async (post) => {
36 const comments = await fetch(\`/api/posts/\${post.id}/comments\`);
37 return { ...post, comments };
38 })
39 );
40
41 return { user, posts: postsWithComments };
42};
43
44// GraphQL Example: Single request
45const graphqlQuery = \`
46 query GetUserWithPosts($userId: ID!) {
47 user(id: $userId) {
48 id
49 name
50 email
51 posts {
52 id
53 title
54 content
55 comments {
56 id
57 content
58 author {
59 name
60 }
61 }
62 }
63 }
64 }
65\`;
66
67const graphqlApproach = async () => {
68 const response = await fetch('/graphql', {
69 method: 'POST',
70 headers: { 'Content-Type': 'application/json' },
71 body: JSON.stringify({
72 query: graphqlQuery,
73 variables: { userId: '123' }
74 })
75 });
76
77 return response.json();
78};Set up Apollo Server with Express.js for a production-ready GraphQL API.
1# Install GraphQL and Apollo Server dependencies
2npm install apollo-server-express graphql
3npm install @graphql-tools/schema graphql-scalars
4npm install graphql-depth-limit graphql-query-complexity
5npm install --save-dev @types/graphql1const { ApolloServer } = require('apollo-server-express');
2const { makeExecutableSchema } = require('@graphql-tools/schema');
3const depthLimit = require('graphql-depth-limit');
4const costAnalysis = require('graphql-query-complexity');
5
6// Import schema and resolvers
7const typeDefs = require('./schema');
8const resolvers = require('./resolvers');
9const { createContext } = require('./context');
10
11// Import plugins
12const authPlugin = require('./plugins/authPlugin');
13const loggingPlugin = require('./plugins/loggingPlugin');
14const cachePlugin = require('./plugins/cachePlugin');
15
16class GraphQLServer {
17 constructor() {
18 this.server = null;
19 this.schema = null;
20 }
21
22 async initialize() {
23 try {
24 // Create executable schema
25 this.schema = makeExecutableSchema({
26 typeDefs,
27 resolvers,
28 schemaDirectives: {
29 // Custom directives can be added here
30 }
31 });
32
33 // Configure Apollo Server
34 this.server = new ApolloServer({
35 schema: this.schema,
36
37 // Context function - runs for every request
38 context: createContext,
39
40 // Enable GraphQL Playground in development
41 introspection: process.env.NODE_ENV !== 'production',
42 playground: process.env.NODE_ENV !== 'production' ? {
43 settings: {
44 'request.credentials': 'include',
45 }
46 } : false,
47
48 // Security and performance plugins
49 plugins: [
50 authPlugin,
51 loggingPlugin,
52 cachePlugin,
53
54 // Custom error formatting
55 {
56 requestDidStart() {
57 return {
58 didEncounterErrors(requestContext) {
59 console.error('GraphQL errors:', requestContext.errors);
60 }
61 };
62 }
63 }
64 ],
65
66 // Query validation rules
67 validationRules: [
68 depthLimit(10), // Prevent deeply nested queries
69 costAnalysis({
70 // Prevent expensive queries
71 maximumCost: 1000,
72 variables: {},
73 createError: (max, actual) => {
74 return new Error(\`Query cost \${actual} exceeds maximum cost \${max}\`);
75 }
76 })
77 ],
78
79 // Format errors for client
80 formatError: (error) => {
81 // Log error details server-side
82 console.error('GraphQL Error:', {
83 message: error.message,
84 locations: error.locations,
85 path: error.path,
86 extensions: error.extensions
87 });
88
89 // Return sanitized error to client
90 if (process.env.NODE_ENV === 'production') {
91 // Don't expose internal errors in production
92 if (error.extensions?.code === 'INTERNAL_ERROR') {
93 return new Error('Internal server error');
94 }
95 }
96
97 return {
98 message: error.message,
99 code: error.extensions?.code,
100 path: error.path,
101 locations: error.locations
102 };
103 },
104
105 // Custom response caching
106 cacheControl: {
107 defaultMaxAge: 300, // 5 minutes default cache
108 },
109
110 // CORS configuration
111 cors: {
112 origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
113 credentials: true
114 }
115 });
116
117 console.log('✅ GraphQL server initialized successfully');
118 return this.server;
119
120 } catch (error) {
121 console.error('❌ GraphQL server initialization failed:', error);
122 throw error;
123 }
124 }
125
126 async start(app) {
127 if (!this.server) {
128 await this.initialize();
129 }
130
131 // Apply Apollo GraphQL middleware to Express
132 this.server.applyMiddleware({
133 app,
134 path: '/graphql',
135 cors: false, // Let Apollo handle CORS
136 bodyParserConfig: {
137 limit: '50mb' // Allow larger payloads for file uploads
138 }
139 });
140
141 console.log(\`🚀 GraphQL server ready at http://localhost:\${process.env.PORT || 4000}\${this.server.graphqlPath}\`);
142 }
143
144 getSchema() {
145 return this.schema;
146 }
147
148 getServer() {
149 return this.server;
150 }
151}
152
153module.exports = new GraphQLServer();Design a robust GraphQL schema with proper types, interfaces, and relationships.
1const { gql } = require('apollo-server-express');
2
3// Import scalar types
4const { GraphQLDateTime, GraphQLEmailType, GraphQLURL } = require('graphql-scalars');
5
6const typeDefs = gql\`
7 # Custom scalar types
8 scalar DateTime
9 scalar Email
10 scalar URL
11 scalar Upload
12
13 # Enums for better type safety
14 enum UserRole {
15 ADMIN
16 MANAGER
17 USER
18 PREMIUM
19 }
20
21 enum OrderStatus {
22 PENDING
23 CONFIRMED
24 PROCESSING
25 SHIPPED
26 DELIVERED
27 CANCELLED
28 }
29
30 enum PaymentStatus {
31 PENDING
32 COMPLETED
33 FAILED
34 REFUNDED
35 }
36
37 enum ProductStatus {
38 DRAFT
39 PUBLISHED
40 ARCHIVED
41 }
42
43 enum SortOrder {
44 ASC
45 DESC
46 }
47
48 # Interfaces for polymorphic types
49 interface Node {
50 id: ID!
51 createdAt: DateTime!
52 updatedAt: DateTime!
53 }
54
55 interface Timestamped {
56 createdAt: DateTime!
57 updatedAt: DateTime!
58 }
59
60 # User types
61 type User implements Node & Timestamped {
62 id: ID!
63 firstName: String!
64 lastName: String!
65 email: Email!
66 avatar: URL
67 role: UserRole!
68 isActive: Boolean!
69 isEmailVerified: Boolean!
70 lastLogin: DateTime
71 createdAt: DateTime!
72 updatedAt: DateTime!
73
74 # Computed fields
75 fullName: String!
76 initials: String!
77
78 # Relationships
79 orders(
80 status: OrderStatus
81 first: Int = 10
82 after: String
83 orderBy: OrderSortInput
84 ): OrderConnection!
85
86 reviews(
87 first: Int = 10
88 after: String
89 ): ReviewConnection!
90
91 # Statistics
92 stats: UserStats!
93 }
94
95 type UserStats {
96 totalOrders: Int!
97 totalSpent: Float!
98 averageOrderValue: Float!
99 favoriteCategories: [Category!]!
100 }
101
102 # Product types
103 type Product implements Node & Timestamped {
104 id: ID!
105 name: String!
106 slug: String!
107 description: String
108 shortDescription: String
109 sku: String!
110 price: Float!
111 compareAtPrice: Float
112 status: ProductStatus!
113 isActive: Boolean!
114 isFeatured: Boolean!
115 createdAt: DateTime!
116 updatedAt: DateTime!
117
118 # Relationships
119 category: Category!
120 variants: [ProductVariant!]!
121 images: [ProductImage!]!
122 reviews(
123 rating: Int
124 first: Int = 10
125 after: String
126 orderBy: ReviewSortInput
127 ): ReviewConnection!
128
129 # Computed fields
130 averageRating: Float
131 totalReviews: Int!
132 discountPercentage: Int
133 stockStatus: StockStatus!
134
135 # Inventory
136 inventory: ProductInventory!
137 }
138
139 type ProductVariant implements Node & Timestamped {
140 id: ID!
141 title: String!
142 sku: String!
143 price: Float!
144 compareAtPrice: Float
145 inventoryQuantity: Int!
146 isActive: Boolean!
147 createdAt: DateTime!
148 updatedAt: DateTime!
149
150 # Option values (size, color, etc.)
151 optionValues: [OptionValue!]!
152
153 # Parent product
154 product: Product!
155 }
156
157 type ProductImage {
158 id: ID!
159 url: URL!
160 alt: String
161 position: Int!
162 }
163
164 type ProductInventory {
165 quantity: Int!
166 trackInventory: Boolean!
167 allowBackorder: Boolean!
168 stockStatus: StockStatus!
169 }
170
171 enum StockStatus {
172 IN_STOCK
173 LOW_STOCK
174 OUT_OF_STOCK
175 BACKORDER
176 }
177
178 type OptionValue {
179 name: String!
180 value: String!
181 }
182
183 # Category types
184 type Category implements Node & Timestamped {
185 id: ID!
186 name: String!
187 slug: String!
188 description: String
189 imageUrl: URL
190 isActive: Boolean!
191 sortOrder: Int!
192 createdAt: DateTime!
193 updatedAt: DateTime!
194
195 # Hierarchy
196 parent: Category
197 children: [Category!]!
198
199 # Products in this category
200 products(
201 status: ProductStatus
202 first: Int = 20
203 after: String
204 orderBy: ProductSortInput
205 ): ProductConnection!
206
207 # Statistics
208 productCount: Int!
209 }
210
211 # Order types
212 type Order implements Node & Timestamped {
213 id: ID!
214 orderNumber: String!
215 status: OrderStatus!
216 paymentStatus: PaymentStatus!
217 subtotal: Float!
218 taxAmount: Float!
219 shippingAmount: Float!
220 discountAmount: Float!
221 totalAmount: Float!
222 createdAt: DateTime!
223 updatedAt: DateTime!
224
225 # Relationships
226 user: User!
227 items: [OrderItem!]!
228
229 # Addresses
230 shippingAddress: Address!
231 billingAddress: Address
232
233 # Tracking
234 trackingNumber: String
235 shippedAt: DateTime
236 deliveredAt: DateTime
237
238 # Notes
239 customerNotes: String
240 adminNotes: String
241 }
242
243 type OrderItem {
244 id: ID!
245 quantity: Int!
246 price: Float!
247 total: Float!
248
249 # Product snapshot at time of order
250 product: Product!
251 variant: ProductVariant
252 productSnapshot: ProductSnapshot!
253 }
254
255 type ProductSnapshot {
256 name: String!
257 description: String
258 imageUrl: URL
259 price: Float!
260 }
261
262 type Address {
263 line1: String!
264 line2: String
265 city: String!
266 state: String!
267 postalCode: String!
268 country: String!
269 }
270
271 # Review types
272 type Review implements Node & Timestamped {
273 id: ID!
274 rating: Int!
275 title: String
276 content: String
277 isVerified: Boolean!
278 isApproved: Boolean!
279 helpfulCount: Int!
280 totalVotes: Int!
281 createdAt: DateTime!
282 updatedAt: DateTime!
283
284 # Relationships
285 user: User!
286 product: Product!
287 order: Order
288 }
289
290 # Connection types for pagination
291 type PageInfo {
292 hasNextPage: Boolean!
293 hasPreviousPage: Boolean!
294 startCursor: String
295 endCursor: String
296 }
297
298 type UserConnection {
299 edges: [UserEdge!]!
300 pageInfo: PageInfo!
301 totalCount: Int!
302 }
303
304 type UserEdge {
305 node: User!
306 cursor: String!
307 }
308
309 type ProductConnection {
310 edges: [ProductEdge!]!
311 pageInfo: PageInfo!
312 totalCount: Int!
313 }
314
315 type ProductEdge {
316 node: Product!
317 cursor: String!
318 }
319
320 type OrderConnection {
321 edges: [OrderEdge!]!
322 pageInfo: PageInfo!
323 totalCount: Int!
324 }
325
326 type OrderEdge {
327 node: Order!
328 cursor: String!
329 }
330
331 type ReviewConnection {
332 edges: [ReviewEdge!]!
333 pageInfo: PageInfo!
334 totalCount: Int!
335 }
336
337 type ReviewEdge {
338 node: Review!
339 cursor: String!
340 }
341
342 # Input types for mutations
343 input CreateUserInput {
344 firstName: String!
345 lastName: String!
346 email: Email!
347 password: String!
348 role: UserRole = USER
349 }
350
351 input UpdateUserInput {
352 firstName: String
353 lastName: String
354 avatar: URL
355 }
356
357 input CreateProductInput {
358 name: String!
359 description: String
360 shortDescription: String
361 price: Float!
362 compareAtPrice: Float
363 categoryId: ID!
364 images: [Upload!]
365 variants: [CreateProductVariantInput!]
366 }
367
368 input CreateProductVariantInput {
369 title: String!
370 price: Float!
371 inventoryQuantity: Int!
372 optionValues: [OptionValueInput!]!
373 }
374
375 input OptionValueInput {
376 name: String!
377 value: String!
378 }
379
380 input UpdateProductInput {
381 name: String
382 description: String
383 shortDescription: String
384 price: Float
385 compareAtPrice: Float
386 status: ProductStatus
387 isFeatured: Boolean
388 }
389
390 input CreateOrderInput {
391 items: [OrderItemInput!]!
392 shippingAddress: AddressInput!
393 billingAddress: AddressInput
394 customerNotes: String
395 }
396
397 input OrderItemInput {
398 productId: ID!
399 variantId: ID
400 quantity: Int!
401 }
402
403 input AddressInput {
404 line1: String!
405 line2: String
406 city: String!
407 state: String!
408 postalCode: String!
409 country: String!
410 }
411
412 input CreateReviewInput {
413 productId: ID!
414 orderId: ID
415 rating: Int!
416 title: String
417 content: String
418 }
419
420 # Sort inputs
421 input UserSortInput {
422 field: UserSortField!
423 order: SortOrder!
424 }
425
426 enum UserSortField {
427 CREATED_AT
428 UPDATED_AT
429 FIRST_NAME
430 LAST_NAME
431 EMAIL
432 }
433
434 input ProductSortInput {
435 field: ProductSortField!
436 order: SortOrder!
437 }
438
439 enum ProductSortField {
440 CREATED_AT
441 UPDATED_AT
442 NAME
443 PRICE
444 RATING
445 }
446
447 input OrderSortInput {
448 field: OrderSortField!
449 order: SortOrder!
450 }
451
452 enum OrderSortField {
453 CREATED_AT
454 UPDATED_AT
455 ORDER_NUMBER
456 TOTAL_AMOUNT
457 }
458
459 input ReviewSortInput {
460 field: ReviewSortField!
461 order: SortOrder!
462 }
463
464 enum ReviewSortField {
465 CREATED_AT
466 RATING
467 HELPFUL_COUNT
468 }
469
470 # Filter inputs
471 input ProductFilterInput {
472 categoryId: ID
473 priceRange: PriceRangeInput
474 rating: Int
475 inStock: Boolean
476 status: ProductStatus
477 search: String
478 }
479
480 input PriceRangeInput {
481 min: Float
482 max: Float
483 }
484
485 input OrderFilterInput {
486 status: OrderStatus
487 paymentStatus: PaymentStatus
488 dateRange: DateRangeInput
489 }
490
491 input DateRangeInput {
492 start: DateTime!
493 end: DateTime!
494 }
495
496 # Root types
497 type Query {
498 # User queries
499 me: User
500 user(id: ID!): User
501 users(
502 first: Int = 20
503 after: String
504 orderBy: UserSortInput
505 search: String
506 ): UserConnection!
507
508 # Product queries
509 product(id: ID, slug: String): Product
510 products(
511 first: Int = 20
512 after: String
513 orderBy: ProductSortInput
514 filter: ProductFilterInput
515 ): ProductConnection!
516
517 searchProducts(
518 query: String!
519 first: Int = 20
520 after: String
521 ): ProductConnection!
522
523 # Category queries
524 category(id: ID, slug: String): Category
525 categories: [Category!]!
526
527 # Order queries
528 order(id: ID!): Order
529 orders(
530 first: Int = 20
531 after: String
532 orderBy: OrderSortInput
533 filter: OrderFilterInput
534 ): OrderConnection!
535
536 # Review queries
537 review(id: ID!): Review
538 reviews(
539 first: Int = 20
540 after: String
541 orderBy: ReviewSortInput
542 productId: ID
543 ): ReviewConnection!
544 }
545
546 type Mutation {
547 # Authentication
548 login(email: Email!, password: String!): AuthPayload!
549 register(input: CreateUserInput!): AuthPayload!
550 logout: Boolean!
551 refreshToken: AuthPayload!
552
553 # User mutations
554 updateProfile(input: UpdateUserInput!): User!
555 changePassword(currentPassword: String!, newPassword: String!): Boolean!
556 deleteAccount: Boolean!
557
558 # Product mutations
559 createProduct(input: CreateProductInput!): Product!
560 updateProduct(id: ID!, input: UpdateProductInput!): Product!
561 deleteProduct(id: ID!): Boolean!
562 publishProduct(id: ID!): Product!
563 archiveProduct(id: ID!): Product!
564
565 # Order mutations
566 createOrder(input: CreateOrderInput!): Order!
567 updateOrderStatus(id: ID!, status: OrderStatus!): Order!
568 cancelOrder(id: ID!): Order!
569
570 # Review mutations
571 createReview(input: CreateReviewInput!): Review!
572 updateReview(id: ID!, rating: Int, title: String, content: String): Review!
573 deleteReview(id: ID!): Boolean!
574
575 # Review moderation
576 approveReview(id: ID!): Review!
577 rejectReview(id: ID!): Review!
578 }
579
580 type Subscription {
581 # Order subscriptions
582 orderUpdated(userId: ID): Order!
583 orderStatusChanged(orderId: ID!): Order!
584
585 # Product subscriptions
586 productUpdated(productId: ID): Product!
587 newProduct(categoryId: ID): Product!
588
589 # Review subscriptions
590 newReview(productId: ID!): Review!
591 reviewModerated(productId: ID): Review!
592
593 # User subscriptions
594 userOnline: User!
595 userOffline: User!
596 }
597
598 type AuthPayload {
599 token: String!
600 user: User!
601 expiresIn: Int!
602 }
603\`;
604
605module.exports = typeDefs;Implement comprehensive resolvers for queries, mutations, and subscriptions.
1const { GraphQLDateTime, GraphQLEmailType, GraphQLURL } = require('graphql-scalars');
2const { withFilter } = require('graphql-subscriptions');
3const { combineResolvers } = require('graphql-resolvers');
4
5// Import individual resolvers
6const userResolvers = require('./user');
7const productResolvers = require('./product');
8const orderResolvers = require('./order');
9const reviewResolvers = require('./review');
10const categoryResolvers = require('./category');
11const authResolvers = require('./auth');
12
13// Import services
14const { pubsub } = require('../pubsub');
15
16// Import authentication and authorization
17const { isAuthenticated, isAuthorized } = require('../auth');
18
19const resolvers = {
20 // Custom scalar resolvers
21 DateTime: GraphQLDateTime,
22 Email: GraphQLEmailType,
23 URL: GraphQLURL,
24
25 // Interface resolvers
26 Node: {
27 __resolveType(obj) {
28 if (obj.email) return 'User';
29 if (obj.sku) return 'Product';
30 if (obj.orderNumber) return 'Order';
31 if (obj.rating) return 'Review';
32 return null;
33 }
34 },
35
36 Query: {
37 // User queries
38 me: combineResolvers(isAuthenticated, userResolvers.me),
39 user: userResolvers.user,
40 users: combineResolvers(
41 isAuthenticated,
42 isAuthorized(['ADMIN', 'MANAGER']),
43 userResolvers.users
44 ),
45
46 // Product queries
47 product: productResolvers.product,
48 products: productResolvers.products,
49 searchProducts: productResolvers.searchProducts,
50
51 // Category queries
52 category: categoryResolvers.category,
53 categories: categoryResolvers.categories,
54
55 // Order queries
56 order: combineResolvers(isAuthenticated, orderResolvers.order),
57 orders: combineResolvers(isAuthenticated, orderResolvers.orders),
58
59 // Review queries
60 review: reviewResolvers.review,
61 reviews: reviewResolvers.reviews
62 },
63
64 Mutation: {
65 // Authentication
66 login: authResolvers.login,
67 register: authResolvers.register,
68 logout: combineResolvers(isAuthenticated, authResolvers.logout),
69 refreshToken: authResolvers.refreshToken,
70
71 // User mutations
72 updateProfile: combineResolvers(isAuthenticated, userResolvers.updateProfile),
73 changePassword: combineResolvers(isAuthenticated, userResolvers.changePassword),
74 deleteAccount: combineResolvers(isAuthenticated, userResolvers.deleteAccount),
75
76 // Product mutations
77 createProduct: combineResolvers(
78 isAuthenticated,
79 isAuthorized(['ADMIN', 'MANAGER']),
80 productResolvers.createProduct
81 ),
82 updateProduct: combineResolvers(
83 isAuthenticated,
84 isAuthorized(['ADMIN', 'MANAGER']),
85 productResolvers.updateProduct
86 ),
87 deleteProduct: combineResolvers(
88 isAuthenticated,
89 isAuthorized(['ADMIN']),
90 productResolvers.deleteProduct
91 ),
92 publishProduct: combineResolvers(
93 isAuthenticated,
94 isAuthorized(['ADMIN', 'MANAGER']),
95 productResolvers.publishProduct
96 ),
97 archiveProduct: combineResolvers(
98 isAuthenticated,
99 isAuthorized(['ADMIN', 'MANAGER']),
100 productResolvers.archiveProduct
101 ),
102
103 // Order mutations
104 createOrder: combineResolvers(isAuthenticated, orderResolvers.createOrder),
105 updateOrderStatus: combineResolvers(
106 isAuthenticated,
107 isAuthorized(['ADMIN', 'MANAGER']),
108 orderResolvers.updateOrderStatus
109 ),
110 cancelOrder: combineResolvers(isAuthenticated, orderResolvers.cancelOrder),
111
112 // Review mutations
113 createReview: combineResolvers(isAuthenticated, reviewResolvers.createReview),
114 updateReview: combineResolvers(isAuthenticated, reviewResolvers.updateReview),
115 deleteReview: combineResolvers(isAuthenticated, reviewResolvers.deleteReview),
116 approveReview: combineResolvers(
117 isAuthenticated,
118 isAuthorized(['ADMIN', 'MANAGER']),
119 reviewResolvers.approveReview
120 ),
121 rejectReview: combineResolvers(
122 isAuthenticated,
123 isAuthorized(['ADMIN', 'MANAGER']),
124 reviewResolvers.rejectReview
125 )
126 },
127
128 Subscription: {
129 // Order subscriptions
130 orderUpdated: {
131 subscribe: withFilter(
132 () => pubsub.asyncIterator(['ORDER_UPDATED']),
133 (payload, variables, context) => {
134 // Only send updates to the order owner or admins
135 return (
136 !variables.userId ||
137 payload.orderUpdated.userId === variables.userId ||
138 context.user?.role === 'ADMIN'
139 );
140 }
141 )
142 },
143
144 orderStatusChanged: {
145 subscribe: withFilter(
146 () => pubsub.asyncIterator(['ORDER_STATUS_CHANGED']),
147 (payload, variables, context) => {
148 return payload.orderStatusChanged.id === variables.orderId;
149 }
150 )
151 },
152
153 // Product subscriptions
154 productUpdated: {
155 subscribe: withFilter(
156 () => pubsub.asyncIterator(['PRODUCT_UPDATED']),
157 (payload, variables) => {
158 return !variables.productId || payload.productUpdated.id === variables.productId;
159 }
160 )
161 },
162
163 newProduct: {
164 subscribe: withFilter(
165 () => pubsub.asyncIterator(['NEW_PRODUCT']),
166 (payload, variables) => {
167 return !variables.categoryId || payload.newProduct.categoryId === variables.categoryId;
168 }
169 )
170 },
171
172 // Review subscriptions
173 newReview: {
174 subscribe: withFilter(
175 () => pubsub.asyncIterator(['NEW_REVIEW']),
176 (payload, variables) => {
177 return payload.newReview.productId === variables.productId;
178 }
179 )
180 },
181
182 reviewModerated: {
183 subscribe: withFilter(
184 () => pubsub.asyncIterator(['REVIEW_MODERATED']),
185 (payload, variables) => {
186 return !variables.productId || payload.reviewModerated.productId === variables.productId;
187 }
188 )
189 },
190
191 // User subscriptions
192 userOnline: {
193 subscribe: () => pubsub.asyncIterator(['USER_ONLINE'])
194 },
195
196 userOffline: {
197 subscribe: () => pubsub.asyncIterator(['USER_OFFLINE'])
198 }
199 },
200
201 // Field resolvers for complex types
202 User: {
203 fullName: (user) => \`\${user.firstName} \${user.lastName}\`,
204 initials: (user) => \`\${user.firstName[0]}\${user.lastName[0]}\`.toUpperCase(),
205
206 orders: async (user, args, context) => {
207 return await context.dataSources.orderAPI.getOrdersByUser(user.id, args);
208 },
209
210 reviews: async (user, args, context) => {
211 return await context.dataSources.reviewAPI.getReviewsByUser(user.id, args);
212 },
213
214 stats: async (user, args, context) => {
215 return await context.dataSources.userAPI.getUserStats(user.id);
216 }
217 },
218
219 Product: {
220 category: async (product, args, context) => {
221 return await context.dataSources.categoryAPI.getCategoryById(product.categoryId);
222 },
223
224 variants: async (product, args, context) => {
225 return await context.dataSources.productAPI.getProductVariants(product.id);
226 },
227
228 images: async (product, args, context) => {
229 return await context.dataSources.productAPI.getProductImages(product.id);
230 },
231
232 reviews: async (product, args, context) => {
233 return await context.dataSources.reviewAPI.getReviewsByProduct(product.id, args);
234 },
235
236 averageRating: async (product, args, context) => {
237 const stats = await context.dataSources.reviewAPI.getProductReviewStats(product.id);
238 return stats.averageRating;
239 },
240
241 totalReviews: async (product, args, context) => {
242 const stats = await context.dataSources.reviewAPI.getProductReviewStats(product.id);
243 return stats.totalReviews;
244 },
245
246 discountPercentage: (product) => {
247 if (product.compareAtPrice && product.compareAtPrice > product.price) {
248 return Math.round((1 - product.price / product.compareAtPrice) * 100);
249 }
250 return 0;
251 },
252
253 stockStatus: (product) => {
254 if (!product.trackInventory) return 'IN_STOCK';
255 if (product.inventoryQuantity > 10) return 'IN_STOCK';
256 if (product.inventoryQuantity > 0) return 'LOW_STOCK';
257 return 'OUT_OF_STOCK';
258 },
259
260 inventory: (product) => ({
261 quantity: product.inventoryQuantity || 0,
262 trackInventory: product.trackInventory || false,
263 allowBackorder: product.allowBackorder || false,
264 stockStatus: product.stockStatus || 'IN_STOCK'
265 })
266 },
267
268 ProductVariant: {
269 product: async (variant, args, context) => {
270 return await context.dataSources.productAPI.getProductById(variant.productId);
271 },
272
273 optionValues: (variant) => {
274 return Object.entries(variant.optionValues || {}).map(([name, value]) => ({
275 name,
276 value
277 }));
278 }
279 },
280
281 Category: {
282 parent: async (category, args, context) => {
283 if (!category.parentId) return null;
284 return await context.dataSources.categoryAPI.getCategoryById(category.parentId);
285 },
286
287 children: async (category, args, context) => {
288 return await context.dataSources.categoryAPI.getCategoryChildren(category.id);
289 },
290
291 products: async (category, args, context) => {
292 return await context.dataSources.productAPI.getProductsByCategory(category.id, args);
293 },
294
295 productCount: async (category, args, context) => {
296 const stats = await context.dataSources.categoryAPI.getCategoryStats(category.id);
297 return stats.productCount;
298 }
299 },
300
301 Order: {
302 user: async (order, args, context) => {
303 return await context.dataSources.userAPI.getUserById(order.userId);
304 },
305
306 items: async (order, args, context) => {
307 return await context.dataSources.orderAPI.getOrderItems(order.id);
308 },
309
310 shippingAddress: (order) => JSON.parse(order.shippingAddress),
311 billingAddress: (order) => order.billingAddress ? JSON.parse(order.billingAddress) : null
312 },
313
314 OrderItem: {
315 product: async (item, args, context) => {
316 return await context.dataSources.productAPI.getProductById(item.productId);
317 },
318
319 variant: async (item, args, context) => {
320 if (!item.productVariantId) return null;
321 return await context.dataSources.productAPI.getProductVariantById(item.productVariantId);
322 },
323
324 productSnapshot: (item) => JSON.parse(item.productSnapshot)
325 },
326
327 Review: {
328 user: async (review, args, context) => {
329 return await context.dataSources.userAPI.getUserById(review.userId);
330 },
331
332 product: async (review, args, context) => {
333 return await context.dataSources.productAPI.getProductById(review.productId);
334 },
335
336 order: async (review, args, context) => {
337 if (!review.orderId) return null;
338 return await context.dataSources.orderAPI.getOrderById(review.orderId);
339 }
340 }
341};
342
343module.exports = resolvers;Implement the Data Source pattern with DataLoader to solve the N+1 query problem.
1const { DataSource } = require('apollo-datasource');
2const DataLoader = require('dataloader');
3
4class ProductAPI extends DataSource {
5 constructor() {
6 super();
7 this.productService = require('../../services/productService');
8 this.categoryService = require('../../services/categoryService');
9 this.reviewService = require('../../services/reviewService');
10 }
11
12 initialize(config) {
13 this.context = config.context;
14 this.cache = config.cache;
15
16 // Initialize DataLoaders to solve N+1 problem
17 this.productLoader = new DataLoader(
18 async (ids) => {
19 const products = await this.productService.getProductsByIds(ids);
20 // Return products in the same order as requested IDs
21 return ids.map(id => products.find(product => product.id === id) || null);
22 },
23 {
24 cache: true,
25 cacheKeyFn: (key) => \`product:\${key}\`,
26 batchScheduleFn: (callback) => setTimeout(callback, 10) // Batch requests within 10ms
27 }
28 );
29
30 this.categoryLoader = new DataLoader(
31 async (ids) => {
32 const categories = await this.categoryService.getCategoriesByIds(ids);
33 return ids.map(id => categories.find(category => category.id === id) || null);
34 },
35 { cache: true }
36 );
37
38 this.productVariantsLoader = new DataLoader(
39 async (productIds) => {
40 const variantsByProduct = await this.productService.getVariantsByProductIds(productIds);
41 return productIds.map(productId => variantsByProduct[productId] || []);
42 },
43 { cache: true }
44 );
45
46 this.productImagesLoader = new DataLoader(
47 async (productIds) => {
48 const imagesByProduct = await this.productService.getImagesByProductIds(productIds);
49 return productIds.map(productId => imagesByProduct[productId] || []);
50 },
51 { cache: true }
52 );
53
54 this.reviewStatsLoader = new DataLoader(
55 async (productIds) => {
56 const statsByProduct = await this.reviewService.getStatsByProductIds(productIds);
57 return productIds.map(productId => statsByProduct[productId] || {
58 averageRating: 0,
59 totalReviews: 0
60 });
61 },
62 { cache: true }
63 );
64 }
65
66 // Product queries
67 async getProductById(id) {
68 try {
69 return await this.productLoader.load(id);
70 } catch (error) {
71 console.error(\`Error loading product \${id}:\`, error);
72 return null;
73 }
74 }
75
76 async getProducts(args) {
77 const {
78 first = 20,
79 after,
80 orderBy = { field: 'CREATED_AT', order: 'DESC' },
81 filter = {}
82 } = args;
83
84 try {
85 const result = await this.productService.getProducts({
86 limit: first,
87 cursor: after,
88 sortBy: orderBy.field.toLowerCase(),
89 sortOrder: orderBy.order.toLowerCase(),
90 filters: this.transformFilters(filter)
91 });
92
93 // Pre-load related data to avoid N+1 queries
94 const productIds = result.products.map(p => p.id);
95 await Promise.all([
96 this.categoryLoader.loadMany(result.products.map(p => p.categoryId)),
97 this.productVariantsLoader.loadMany(productIds),
98 this.productImagesLoader.loadMany(productIds),
99 this.reviewStatsLoader.loadMany(productIds)
100 ]);
101
102 return this.formatConnection(result);
103 } catch (error) {
104 console.error('Error loading products:', error);
105 throw new Error('Failed to load products');
106 }
107 }
108
109 async searchProducts(args) {
110 const { query, first = 20, after } = args;
111
112 try {
113 const result = await this.productService.searchProducts({
114 query,
115 limit: first,
116 cursor: after
117 });
118
119 // Pre-load related data
120 const productIds = result.products.map(p => p.id);
121 await Promise.all([
122 this.categoryLoader.loadMany(result.products.map(p => p.categoryId)),
123 this.reviewStatsLoader.loadMany(productIds)
124 ]);
125
126 return this.formatConnection(result);
127 } catch (error) {
128 console.error('Error searching products:', error);
129 throw new Error('Search failed');
130 }
131 }
132
133 async getProductsByCategory(categoryId, args) {
134 const {
135 first = 20,
136 after,
137 orderBy = { field: 'CREATED_AT', order: 'DESC' },
138 status
139 } = args;
140
141 try {
142 const result = await this.productService.getProductsByCategory(categoryId, {
143 limit: first,
144 cursor: after,
145 sortBy: orderBy.field.toLowerCase(),
146 sortOrder: orderBy.order.toLowerCase(),
147 status: status?.toLowerCase()
148 });
149
150 return this.formatConnection(result);
151 } catch (error) {
152 console.error(\`Error loading products for category \${categoryId}:\`, error);
153 throw new Error('Failed to load category products');
154 }
155 }
156
157 // Product variant queries
158 async getProductVariants(productId) {
159 try {
160 return await this.productVariantsLoader.load(productId);
161 } catch (error) {
162 console.error(\`Error loading variants for product \${productId}:\`, error);
163 return [];
164 }
165 }
166
167 async getProductVariantById(id) {
168 try {
169 return await this.productService.getVariantById(id);
170 } catch (error) {
171 console.error(\`Error loading variant \${id}:\`, error);
172 return null;
173 }
174 }
175
176 // Product image queries
177 async getProductImages(productId) {
178 try {
179 return await this.productImagesLoader.load(productId);
180 } catch (error) {
181 console.error(\`Error loading images for product \${productId}:\`, error);
182 return [];
183 }
184 }
185
186 // Product mutations
187 async createProduct(input) {
188 try {
189 const product = await this.productService.createProduct({
190 ...input,
191 createdBy: this.context.user.id
192 });
193
194 // Clear relevant caches
195 this.clearProductCaches();
196
197 // Publish subscription event
198 this.context.pubsub.publish('NEW_PRODUCT', { newProduct: product });
199
200 return product;
201 } catch (error) {
202 console.error('Error creating product:', error);
203 throw new Error(\`Failed to create product: \${error.message}\`);
204 }
205 }
206
207 async updateProduct(id, input) {
208 try {
209 const product = await this.productService.updateProduct(id, {
210 ...input,
211 updatedBy: this.context.user.id
212 });
213
214 if (!product) {
215 throw new Error('Product not found');
216 }
217
218 // Clear caches for this product
219 this.productLoader.clear(id);
220 this.clearProductCaches();
221
222 // Publish subscription event
223 this.context.pubsub.publish('PRODUCT_UPDATED', { productUpdated: product });
224
225 return product;
226 } catch (error) {
227 console.error(\`Error updating product \${id}:\`, error);
228 throw new Error(\`Failed to update product: \${error.message}\`);
229 }
230 }
231
232 async deleteProduct(id) {
233 try {
234 const success = await this.productService.deleteProduct(id, {
235 deletedBy: this.context.user.id
236 });
237
238 if (success) {
239 // Clear caches
240 this.productLoader.clear(id);
241 this.clearProductCaches();
242 }
243
244 return success;
245 } catch (error) {
246 console.error(\`Error deleting product \${id}:\`, error);
247 throw new Error(\`Failed to delete product: \${error.message}\`);
248 }
249 }
250
251 async publishProduct(id) {
252 try {
253 const product = await this.productService.updateProductStatus(id, 'PUBLISHED', {
254 updatedBy: this.context.user.id
255 });
256
257 // Clear caches
258 this.productLoader.clear(id);
259 this.clearProductCaches();
260
261 return product;
262 } catch (error) {
263 console.error(\`Error publishing product \${id}:\`, error);
264 throw new Error('Failed to publish product');
265 }
266 }
267
268 async archiveProduct(id) {
269 try {
270 const product = await this.productService.updateProductStatus(id, 'ARCHIVED', {
271 updatedBy: this.context.user.id
272 });
273
274 // Clear caches
275 this.productLoader.clear(id);
276 this.clearProductCaches();
277
278 return product;
279 } catch (error) {
280 console.error(\`Error archiving product \${id}:\`, error);
281 throw new Error('Failed to archive product');
282 }
283 }
284
285 // Helper methods
286 transformFilters(filter) {
287 const transformed = {};
288
289 if (filter.categoryId) {
290 transformed.categoryId = filter.categoryId;
291 }
292
293 if (filter.priceRange) {
294 transformed.priceRange = {
295 min: filter.priceRange.min,
296 max: filter.priceRange.max
297 };
298 }
299
300 if (filter.rating) {
301 transformed.minRating = filter.rating;
302 }
303
304 if (filter.inStock !== undefined) {
305 transformed.inStock = filter.inStock;
306 }
307
308 if (filter.status) {
309 transformed.status = filter.status.toLowerCase();
310 }
311
312 if (filter.search) {
313 transformed.search = filter.search;
314 }
315
316 return transformed;
317 }
318
319 formatConnection(result) {
320 const edges = result.products.map((product, index) => ({
321 node: product,
322 cursor: this.encodeCursor({
323 id: product.id,
324 createdAt: product.createdAt
325 })
326 }));
327
328 return {
329 edges,
330 pageInfo: {
331 hasNextPage: result.pagination.hasNextPage,
332 hasPreviousPage: result.pagination.hasPrevPage,
333 startCursor: edges.length > 0 ? edges[0].cursor : null,
334 endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null
335 },
336 totalCount: result.pagination.totalCount
337 };
338 }
339
340 encodeCursor(data) {
341 return Buffer.from(JSON.stringify(data)).toString('base64');
342 }
343
344 decodeCursor(cursor) {
345 try {
346 return JSON.parse(Buffer.from(cursor, 'base64').toString());
347 } catch (error) {
348 throw new Error('Invalid cursor');
349 }
350 }
351
352 clearProductCaches() {
353 // Clear all product-related caches
354 this.productLoader.clearAll();
355 this.productVariantsLoader.clearAll();
356 this.productImagesLoader.clearAll();
357 this.reviewStatsLoader.clearAll();
358 }
359
360 // Cache management with TTL
361 async getCachedProductStats(productId) {
362 const cacheKey = \`product:stats:\${productId}\`;
363 const cached = await this.cache.get(cacheKey);
364
365 if (cached) {
366 return JSON.parse(cached);
367 }
368
369 const stats = await this.reviewService.getProductStats(productId);
370
371 // Cache for 5 minutes
372 await this.cache.set(cacheKey, JSON.stringify(stats), { ttl: 300 });
373
374 return stats;
375 }
376}
377
378module.exports = ProductAPI;Implement GraphQL subscriptions for real-time features using pub/sub patterns.
1const { PubSub } = require('graphql-subscriptions');
2const { RedisPubSub } = require('graphql-redis-subscriptions');
3const Redis = require('ioredis');
4
5class SubscriptionManager {
6 constructor() {
7 this.pubsub = null;
8 this.redisClient = null;
9 this.initializePubSub();
10 }
11
12 initializePubSub() {
13 if (process.env.NODE_ENV === 'production' && process.env.REDIS_URL) {
14 // Use Redis for production (supports multiple server instances)
15 this.redisClient = new Redis({
16 host: process.env.REDIS_HOST || 'localhost',
17 port: process.env.REDIS_PORT || 6379,
18 password: process.env.REDIS_PASSWORD,
19 db: process.env.REDIS_DB || 0,
20 lazyConnect: true,
21 retryDelayOnFailover: 100,
22 enableReadyCheck: false,
23 maxRetriesPerRequest: null
24 });
25
26 this.pubsub = new RedisPubSub({
27 publisher: this.redisClient,
28 subscriber: this.redisClient.duplicate(),
29 messageEventName: 'message',
30 pmessageEventName: 'pmessage'
31 });
32
33 console.log('✅ Redis PubSub initialized for subscriptions');
34 } else {
35 // Use in-memory PubSub for development
36 this.pubsub = new PubSub();
37 console.log('✅ In-memory PubSub initialized for subscriptions');
38 }
39 }
40
41 // Order events
42 async publishOrderUpdate(order) {
43 await this.pubsub.publish('ORDER_UPDATED', {
44 orderUpdated: order
45 });
46 }
47
48 async publishOrderStatusChange(order) {
49 await this.pubsub.publish('ORDER_STATUS_CHANGED', {
50 orderStatusChanged: order
51 });
52 }
53
54 // Product events
55 async publishProductUpdate(product) {
56 await this.pubsub.publish('PRODUCT_UPDATED', {
57 productUpdated: product
58 });
59 }
60
61 async publishNewProduct(product) {
62 await this.pubsub.publish('NEW_PRODUCT', {
63 newProduct: product
64 });
65 }
66
67 // Review events
68 async publishNewReview(review) {
69 await this.pubsub.publish('NEW_REVIEW', {
70 newReview: review
71 });
72 }
73
74 async publishReviewModerated(review) {
75 await this.pubsub.publish('REVIEW_MODERATED', {
76 reviewModerated: review
77 });
78 }
79
80 // User events
81 async publishUserOnline(user) {
82 await this.pubsub.publish('USER_ONLINE', {
83 userOnline: user
84 });
85 }
86
87 async publishUserOffline(user) {
88 await this.pubsub.publish('USER_OFFLINE', {
89 userOffline: user
90 });
91 }
92
93 // Custom event publisher
94 async publish(event, payload) {
95 try {
96 await this.pubsub.publish(event, payload);
97 console.log(\`📡 Published event: \${event}\`);
98 } catch (error) {
99 console.error(\`❌ Failed to publish event \${event}:\`, error);
100 }
101 }
102
103 // Subscribe to events
104 asyncIterator(events) {
105 return this.pubsub.asyncIterator(events);
106 }
107
108 // Cleanup
109 async close() {
110 if (this.redisClient) {
111 await this.redisClient.quit();
112 }
113 console.log('🔌 PubSub connections closed');
114 }
115}
116
117const subscriptionManager = new SubscriptionManager();
118
119// Export both the manager and the pubsub instance
120module.exports = {
121 subscriptionManager,
122 pubsub: subscriptionManager.pubsub
123};Set up comprehensive GraphQL context with authentication and authorization.
1const jwt = require('jsonwebtoken');
2const { AuthenticationError } = require('apollo-server-express');
3
4// Import data sources
5const UserAPI = require('./dataSources/userAPI');
6const ProductAPI = require('./dataSources/productAPI');
7const OrderAPI = require('./dataSources/orderAPI');
8const ReviewAPI = require('./dataSources/reviewAPI');
9const CategoryAPI = require('./dataSources/categoryAPI');
10
11// Import services
12const userService = require('../services/userService');
13const { pubsub } = require('./pubsub');
14
15async function createContext({ req, res, connection }) {
16 // Handle subscription context (WebSocket)
17 if (connection) {
18 return {
19 ...connection.context,
20 pubsub
21 };
22 }
23
24 // Initialize data sources
25 const dataSources = {
26 userAPI: new UserAPI(),
27 productAPI: new ProductAPI(),
28 orderAPI: new OrderAPI(),
29 reviewAPI: new ReviewAPI(),
30 categoryAPI: new CategoryAPI()
31 };
32
33 // Extract auth token
34 let token = null;
35 const authHeader = req.headers.authorization;
36
37 if (authHeader && authHeader.startsWith('Bearer ')) {
38 token = authHeader.substring(7);
39 } else if (req.cookies && req.cookies.authToken) {
40 token = req.cookies.authToken;
41 }
42
43 // Authenticate user
44 let user = null;
45 let permissions = [];
46
47 if (token) {
48 try {
49 const decoded = jwt.verify(token, process.env.JWT_SECRET);
50 user = await userService.getUserById(decoded.userId);
51
52 if (user && user.isActive) {
53 permissions = await userService.getUserPermissions(user.id);
54 } else {
55 user = null;
56 }
57 } catch (error) {
58 console.warn('Invalid or expired token:', error.message);
59 // Don't throw error here - let resolvers handle authentication
60 }
61 }
62
63 // Create request context
64 const context = {
65 req,
66 res,
67 user,
68 permissions,
69 dataSources,
70 pubsub,
71
72 // Utility functions
73 isAuthenticated: () => {
74 if (!user) {
75 throw new AuthenticationError('Authentication required');
76 }
77 return true;
78 },
79
80 hasPermission: (permission) => {
81 if (!user) return false;
82 return permissions.includes(permission) || user.role === 'ADMIN';
83 },
84
85 requirePermission: (permission) => {
86 if (!context.hasPermission(permission)) {
87 throw new ForbiddenError(\`Permission required: \${permission}\`);
88 }
89 return true;
90 },
91
92 // Request metadata
93 ip: req.ip || req.connection.remoteAddress,
94 userAgent: req.headers['user-agent'],
95
96 // Rate limiting context
97 rateLimit: {
98 remaining: req.rateLimit?.remaining || 1000,
99 reset: req.rateLimit?.reset || Date.now() + 3600000
100 }
101 };
102
103 return context;
104}
105
106// WebSocket authentication for subscriptions
107function onConnect(connectionParams, webSocket, connectionContext) {
108 console.log('🔌 WebSocket connection attempt');
109
110 return new Promise(async (resolve, reject) => {
111 try {
112 // Extract token from connection params
113 const token = connectionParams.authToken || connectionParams.Authorization?.replace('Bearer ', '');
114
115 if (!token) {
116 console.log('❌ WebSocket connection rejected: No token provided');
117 reject(new Error('Authentication token required'));
118 return;
119 }
120
121 // Verify token
122 const decoded = jwt.verify(token, process.env.JWT_SECRET);
123 const user = await userService.getUserById(decoded.userId);
124
125 if (!user || !user.isActive) {
126 console.log('❌ WebSocket connection rejected: Invalid user');
127 reject(new Error('Invalid user'));
128 return;
129 }
130
131 const permissions = await userService.getUserPermissions(user.id);
132
133 console.log(\`✅ WebSocket authenticated: \${user.email}\`);
134
135 // Return context for subscription resolvers
136 resolve({
137 user,
138 permissions,
139 pubsub
140 });
141
142 } catch (error) {
143 console.log('❌ WebSocket authentication failed:', error.message);
144 reject(new Error('Authentication failed'));
145 }
146 });
147}
148
149function onDisconnect(webSocket, connectionContext) {
150 console.log('🔌 WebSocket disconnected');
151}
152
153module.exports = {
154 createContext,
155 onConnect,
156 onDisconnect
157};Learn about Practice Exercises.
Exercise 1: Real-time Chat Application
Build a real-time chat application using GraphQL subscriptions with message threading, user presence, and typing indicators.
Exercise 2: Complex Schema with Relations
Design a comprehensive blog schema with posts, comments, categories, tags, and user relationships using GraphQL best practices.
Exercise 3: GraphQL Federation
Implement Apollo Federation to create a distributed GraphQL architecture with multiple services and unified schema.
Schema Design:
Create type-safe, self-documenting GraphQL schemas
Resolver Patterns:
Implement efficient resolvers with proper error handling
N+1 Prevention:
Use DataLoader to optimize database queries
Real-time Features:
Leverage subscriptions for live updates
Implement authentication, authorization, and query complexity analysis
Performance:
Apply caching strategies and pagination patterns
You've mastered GraphQL integration with Node.js! You can now build modern, flexible APIs with real-time capabilities, efficient data fetching, and robust type safety.
Your progress is saved in this browser, on this device only.