GraphQL Basics
GraphQL is a query language for APIs that enables clients to request exactly the data they need. Learn to build GraphQL servers and optimize API efficiency.
Challenge
Section titled “Challenge”Build a GraphQL API that provides flexible data querying, reduces over-fetching and under-fetching issues, and enables efficient client-side operations.
Explanation
Section titled “Explanation”GraphQL vs REST
Section titled “GraphQL vs REST”REST: Fixed endpoint structure, over/under-fetching GraphQL: Flexible queries, clients request exact data
Core Concepts
Section titled “Core Concepts”Schema: Type definitions for data and queries Resolvers: Functions that return field data Queries: Read operations Mutations: Write operations Subscriptions: Real-time updates
Code Examples
Section titled “Code Examples”1. Basic GraphQL Server
Section titled “1. Basic GraphQL Server”const { ApolloServer, gql } = require('apollo-server-express');const express = require('express');
const app = express();
// GraphQL Schemaconst typeDefs = gql` type User { id: ID! name: String! email: String! posts: [Post!]! }
type Post { id: ID! title: String! content: String! author: User! }
type Query { user(id: ID!): User users: [User!]! post(id: ID!): Post posts: [Post!]! }
type Mutation { createUser(name: String!, email: String!): User! updateUser(id: ID!, name: String): User deleteUser(id: ID!): Boolean! }`;
// Sample dataconst users = [ { id: '1', name: 'John Doe', email: 'john@example.com' }, { id: '2', name: 'Jane Smith', email: 'jane@example.com' },];
const posts = [ { id: '1', title: 'GraphQL Basics', content: 'Learn GraphQL', authorId: '1' }, { id: '2', title: 'Node.js Tips', content: 'Node.js best practices', authorId: '2' },];
// Resolversconst resolvers = { Query: { user: (_, { id }) => users.find(u => u.id === id), users: () => users, post: (_, { id }) => posts.find(p => p.id === id), posts: () => posts, },
Mutation: { createUser: (_, { name, email }) => { const user = { id: String(users.length + 1), name, email }; users.push(user); return user; },
updateUser: (_, { id, name }) => { const user = users.find(u => u.id === id); if (user && name) user.name = name; return user; },
deleteUser: (_, { id }) => { const index = users.findIndex(u => u.id === id); if (index !== -1) { users.splice(index, 1); return true; } return false; }, },
User: { posts: (user) => posts.filter(p => p.authorId === user.id), },
Post: { author: (post) => users.find(u => u.id === post.authorId), },};
// Setup Apollo Server(async () => { const server = new ApolloServer({ typeDefs, resolvers, });
await server.start(); server.applyMiddleware({ app });
app.listen(4000, () => { console.log('GraphQL server running on http://localhost:4000/graphql'); });})();2. Advanced GraphQL with Subscriptions
Section titled “2. Advanced GraphQL with Subscriptions”const { PubSub } = require('apollo-server-express');
const pubsub = new PubSub();
const typeDefs = gql` type Subscription { userCreated: User! postPublished: Post! }`;
const resolvers = { Mutation: { createUser: async (_, { name, email }) => { const user = { id: String(users.length + 1), name, email }; users.push(user);
// Publish subscription event pubsub.publish('USER_CREATED', { userCreated: user }); return user; }, },
Subscription: { userCreated: { subscribe: () => pubsub.asyncIterator(['USER_CREATED']), },
postPublished: { subscribe: () => pubsub.asyncIterator(['POST_PUBLISHED']), }, },};3. DataLoader for Batch Processing
Section titled “3. DataLoader for Batch Processing”const DataLoader = require('dataloader');
// Batch loader functionconst userLoader = new DataLoader(async (userIds) => { // Fetch all users in a single query const userMap = new Map(); const users = await database.users.findByIds(userIds); users.forEach(user => userMap.set(user.id, user));
// Return in same order as input return userIds.map(id => userMap.get(id));});
const resolvers = { Post: { author: (post, _, { userLoader }) => { return userLoader.load(post.authorId); }, },};
// In server setupconst server = new ApolloServer({ typeDefs, resolvers, context: () => ({ userLoader, }),});4. GraphQL Error Handling
Section titled “4. GraphQL Error Handling”const resolvers = { Query: { user: async (_, { id }) => { try { const user = await database.users.findById(id);
if (!user) { throw new Error(`User with ID ${id} not found`); }
return user; } catch (error) { throw new Error(`Failed to fetch user: ${error.message}`); } }, },};
const server = new ApolloServer({ typeDefs, resolvers, formatError: (error) => { console.error('GraphQL Error:', error);
// Don't expose sensitive error details in production if (process.env.NODE_ENV === 'production') { return { message: 'An error occurred', extensions: { code: 'INTERNAL_SERVER_ERROR', }, }; }
return error; },});5. GraphQL Input Types and Validation
Section titled “5. GraphQL Input Types and Validation”const typeDefs = gql` input CreateUserInput { name: String! email: String! }
input UpdateUserInput { name: String email: String }
type Mutation { createUser(input: CreateUserInput!): User! updateUser(id: ID!, input: UpdateUserInput!): User }`;
const resolvers = { Mutation: { createUser: async (_, { input }, context) => { // Validate input if (!input.email.includes('@')) { throw new Error('Invalid email format'); }
if (input.name.length < 2) { throw new Error('Name must be at least 2 characters'); }
const user = await database.users.create(input); return user; },
updateUser: async (_, { id, input }) => { const user = await database.users.findById(id);
if (!user) { throw new Error(`User ${id} not found`); }
Object.assign(user, input); await user.save(); return user; }, },};6. GraphQL Directives
Section titled “6. GraphQL Directives”const typeDefs = gql` directive @auth(roles: [String!]!) on FIELD_DEFINITION directive @deprecated(reason: String) on FIELD_DEFINITION
type User { id: ID! name: String! email: String! admin: Boolean @auth(roles: ["ADMIN"]) }
type Query { users: [User!]! @auth(roles: ["ADMIN", "MODERATOR"]) me: User! }`;
const authDirective = (schema) => { return mapSchema(schema, { 'MapperKind.OBJECT_FIELD': (fieldConfig) => { const authDirectiveArgs = getDirectiveValues(authDirective, fieldConfig);
if (authDirectiveArgs) { const { resolve } = fieldConfig;
fieldConfig.resolve = (obj, args, context, info) => { if (!context.user) { throw new Error('Unauthorized'); }
const userRoles = context.user.roles || []; const hasRole = authDirectiveArgs.roles.some(role => userRoles.includes(role) );
if (!hasRole) { throw new Error('Insufficient permissions'); }
return resolve ? resolve(obj, args, context, info) : obj[info.fieldName]; }; }
return fieldConfig; }, });};Best Practices
Section titled “Best Practices”-
Schema Design
- Clear, descriptive field names
- Proper nullable/non-nullable types
- Reusable fragments and interfaces
-
Performance
- Use DataLoader for batch queries
- Implement query complexity analysis
- Cache frequently accessed data
- Monitor query depth
-
Security
- Authenticate all mutations
- Validate input thoroughly
- Limit query complexity
- Hide sensitive information
-
Developer Experience
- Comprehensive error messages
- Introspection for tooling
- Documentation in schema
- Example queries
-
Monitoring
- Log all operations
- Track query performance
- Monitor resolver execution time
- Alert on anomalies
Real-World Applications
Section titled “Real-World Applications”- Mobile Apps: Bandwidth-efficient queries
- Real-Time Apps: Subscriptions for updates
- Complex Data Fetching: Flexible queries reduce requests
- Internal Tools: Self-documenting APIs
- Microservices: API gateway and federation