← Back to Guides

GraphQL: From Basics to Production

📖 16 min read | 📅 Updated: January 2025 | 🏷️ Backend & APIs

Introduction

GraphQL is a query language for APIs that gives clients the power to ask for exactly what they need. This guide covers schema design, resolvers, mutations, subscriptions, and production best practices with Apollo Server.

1. GraphQL Basics

Setup Apollo Server

# Install dependencies
npm install @apollo/server graphql

# TypeScript support
npm install --save-dev @types/node typescript

# Create server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
  type Query {
    hello: String
  }
`;

const resolvers = {
  Query: {
    hello: () => 'Hello world!',
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`🚀 Server ready at: ${url}`);

2. Schema Definition

Types and Fields

type User {
  id: ID!
  email: String!
  name: String!
  age: Int
  posts: [Post!]!
  createdAt: String!
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  author: User!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

# Query root type
type Query {
  users: [User!]!
  user(id: ID!): User
  posts(published: Boolean): [Post!]!
  post(id: ID!): Post
}

# Mutation root type
type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  createPost(input: CreatePostInput!): Post!
}

# Input types
input CreateUserInput {
  email: String!
  name: String!
  password: String!
}

input UpdateUserInput {
  email: String
  name: String
}

input CreatePostInput {
  title: String!
  content: String!
  authorId: ID!
}

3. Resolvers

Query Resolvers

import User from './models/User';
import Post from './models/Post';

const resolvers = {
  Query: {
    users: async () => {
      return await User.find();
    },
    
    user: async (parent, { id }) => {
      return await User.findById(id);
    },
    
    posts: async (parent, { published }) => {
      const filter = published !== undefined ? { published } : {};
      return await Post.find(filter);
    },
    
    post: async (parent, { id }) => {
      return await Post.findById(id);
    },
  },
  
  // Field resolvers
  User: {
    posts: async (parent) => {
      return await Post.find({ authorId: parent.id });
    },
  },
  
  Post: {
    author: async (parent) => {
      return await User.findById(parent.authorId);
    },
    
    comments: async (parent) => {
      return await Comment.find({ postId: parent.id });
    },
  },
};

Mutation Resolvers

const resolvers = {
  Mutation: {
    createUser: async (parent, { input }, context) => {
      // Validate input
      if (!input.email || !input.password) {
        throw new GraphQLError('Email and password are required', {
          extensions: { code: 'BAD_USER_INPUT' }
        });
      }
      
      // Check if user exists
      const existingUser = await User.findOne({ email: input.email });
      if (existingUser) {
        throw new GraphQLError('User already exists', {
          extensions: { code: 'USER_EXISTS' }
        });
      }
      
      // Hash password
      const hashedPassword = await bcrypt.hash(input.password, 10);
      
      // Create user
      const user = await User.create({
        ...input,
        password: hashedPassword,
      });
      
      return user;
    },
    
    updateUser: async (parent, { id, input }, context) => {
      // Check authentication
      if (!context.user) {
        throw new GraphQLError('Not authenticated', {
          extensions: { code: 'UNAUTHENTICATED' }
        });
      }
      
      // Check authorization
      if (context.user.id !== id) {
        throw new GraphQLError('Not authorized', {
          extensions: { code: 'FORBIDDEN' }
        });
      }
      
      const user = await User.findByIdAndUpdate(
        id,
        { $set: input },
        { new: true }
      );
      
      return user;
    },
    
    deleteUser: async (parent, { id }, context) => {
      await User.findByIdAndDelete(id);
      return true;
    },
  },
};

4. Context & Authentication

import jwt from 'jsonwebtoken';

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    // Get token from header
    const token = req.headers.authorization?.replace('Bearer ', '');
    
    if (token) {
      try {
        // Verify token
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        const user = await User.findById(decoded.userId);
        return { user };
      } catch (error) {
        console.log('Invalid token');
      }
    }
    
    return { user: null };
  },
  listen: { port: 4000 },
});

5. DataLoader (N+1 Problem)

import DataLoader from 'dataloader';

// Create loader
const userLoader = new DataLoader(async (userIds) => {
  const users = await User.find({ _id: { $in: userIds } });
  
  // Return users in same order as requested IDs
  const userMap = {};
  users.forEach(user => {
    userMap[user.id] = user;
  });
  
  return userIds.map(id => userMap[id]);
});

// Use in context
const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    return {
      loaders: {
        user: userLoader,
      },
    };
  },
});

// Use in resolver
const resolvers = {
  Post: {
    author: async (parent, args, context) => {
      return await context.loaders.user.load(parent.authorId);
    },
  },
};

6. Subscriptions

import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

// Add to schema
const typeDefs = `#graphql
  type Subscription {
    postCreated: Post!
    commentAdded(postId: ID!): Comment!
  }
`;

// Add resolvers
const resolvers = {
  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
    },
    
    commentAdded: {
      subscribe: (parent, { postId }) => {
        return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
      },
    },
  },
  
  Mutation: {
    createPost: async (parent, { input }) => {
      const post = await Post.create(input);
      
      // Publish event
      pubsub.publish('POST_CREATED', { postCreated: post });
      
      return post;
    },
  },
};

// Setup WebSocket server
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
});

useServer({ schema }, wsServer);

7. Error Handling

import { GraphQLError } from 'graphql';

// Custom error codes
export enum ErrorCode {
  UNAUTHENTICATED = 'UNAUTHENTICATED',
  FORBIDDEN = 'FORBIDDEN',
  BAD_USER_INPUT = 'BAD_USER_INPUT',
  NOT_FOUND = 'NOT_FOUND',
}

// Helper function
export const throwGraphQLError = (message: string, code: ErrorCode) => {
  throw new GraphQLError(message, {
    extensions: { code }
  });
};

// Usage in resolver
const resolvers = {
  Query: {
    user: async (parent, { id }) => {
      const user = await User.findById(id);
      
      if (!user) {
        throwGraphQLError('User not found', ErrorCode.NOT_FOUND);
      }
      
      return user;
    },
  },
};

// Format errors
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formattedError, error) => {
    // Don't expose internal errors
    if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
      return {
        message: 'Internal server error',
        extensions: {
          code: 'INTERNAL_SERVER_ERROR',
        },
      };
    }
    
    return formattedError;
  },
});

8. Pagination

// Relay-style cursor pagination
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type PostEdge {
  cursor: String!
  node: Post!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type Query {
  posts(first: Int, after: String, last: Int, before: String): PostConnection!
}

// Resolver
const resolvers = {
  Query: {
    posts: async (parent, { first = 10, after }) => {
      const query = after ? { _id: { $gt: after } } : {};
      
      const posts = await Post.find(query)
        .limit(first + 1)
        .sort({ _id: 1 });
      
      const hasNextPage = posts.length > first;
      const edges = posts.slice(0, first).map(post => ({
        cursor: post.id,
        node: post,
      }));
      
      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage: !!after,
          startCursor: edges[0]?.cursor,
          endCursor: edges[edges.length - 1]?.cursor,
        },
        totalCount: await Post.countDocuments(),
      };
    },
  },
};

9. Performance Optimization

// Query complexity
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityLimitRule(1000, {
      onCost: (cost) => console.log('Query cost:', cost),
    }),
  ],
});

// Query depth limiting
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(5)],
});

// Caching with Redis
import { BaseRedisCache } from 'apollo-server-cache-redis';
import Redis from 'ioredis';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  cache: new BaseRedisCache({
    client: new Redis({
      host: 'localhost',
    }),
  }),
  plugins: [
    {
      async requestDidStart() {
        return {
          async willSendResponse({ response, context }) {
            response.http.headers.set(
              'Cache-Control',
              'max-age=60, public'
            );
          },
        };
      },
    },
  ],
});

10. Client Usage

// Apollo Client setup
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
  headers: {
    authorization: `Bearer ${token}`,
  },
});

// Query
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

const { data } = await client.query({ query: GET_USERS });

// Mutation
const CREATE_USER = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
    }
  }
`;

const { data } = await client.mutate({
  mutation: CREATE_USER,
  variables: {
    input: {
      email: 'user@example.com',
      name: 'John Doe',
      password: 'password123',
    },
  },
});

Best Practices

✓ GraphQL Best Practices:

Conclusion

GraphQL provides powerful query capabilities and type safety, making it ideal for modern applications. By implementing DataLoader, proper error handling, authentication, and performance optimizations, you can build scalable GraphQL APIs that provide excellent developer experience.

💡 Pro Tip: Always use DataLoader for any field that fetches related data. Monitor your GraphQL queries in production with Apollo Studio to identify slow resolvers and optimize performance bottlenecks.