GraphQL Complete Guide — Schema, Resolvers, Apollo

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

GraphQL is a query language for APIs. It lets clients request exactly the data they need, nothing more.

GraphQL Basics

# Define types
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
}

# Queries (reading data)
type Query {
  user(id: ID!): User
  users: [User!]!
  post(id: ID!): Post
}

# Mutations (writing data)
type Mutation {
  createUser(name: String!, email: String!): User!
  deleteUser(id: ID!): Boolean!
  createPost(title: String!, content: String!): Post!
}

Apollo Server Setup

npm install @apollo/server graphql
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const typeDefs = `
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    user(id: ID!): User
    users: [User!]!
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
  }
`;

const resolvers = {
  Query: {
    user: (parent, args) => {
      return { id: args.id, name: "Alice", email: "alice@example.com" };
    },
    users: () => [
      { id: "1", name: "Alice", email: "alice@example.com" },
      { id: "2", name: "Bob", email: "bob@example.com" },
    ],
  },
  Mutation: {
    createUser: (parent, args) => {
      return { id: "3", ...args };
    },
  },
};

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

const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`GraphQL server running at ${url}`);

TypeScript Types

import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";

interface User {
  id: string;
  name: string;
  email: string;
}

interface Post {
  id: string;
  title: string;
  content: string;
  author: User;
}

const resolvers = {
  Query: {
    user: (parent: any, args: { id: string }): User | null => {
      // Implementation
      return null;
    },
    users: (): User[] => [],
  },
  User: {
    posts: (parent: User): Post[] => [],
  },
};

Schema Design Patterns

# Connection pattern for pagination
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

# Updated Query
type Query {
  users(first: Int!, after: String): UserConnection!
}

Resolvers

const resolvers = {
  Query: {
    // Nested resolvers for related data
    user: async (parent, args, context) => {
      return await context.db.users.findById(args.id);
    },
  },
  User: {
    // Resolver for nested field
    posts: async (parent, args, context) => {
      return await context.db.posts.findByUserId(parent.id);
    },
  },
};

Mutations

const typeDefs = `
  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
    deleteUser(id: ID!): Boolean!
  }

  input CreateUserInput {
    name: String!
    email: String!
  }

  input UpdateUserInput {
    name: String
    email: String
  }
`;

const resolvers = {
  Mutation: {
    createUser: async (parent, args, context) => {
      const user = await context.db.users.create(args.input);
      return user;
    },
    updateUser: async (parent, args, context) => {
      const user = await context.db.users.update(args.id, args.input);
      return user;
    },
    deleteUser: async (parent, args, context) => {
      await context.db.users.delete(args.id);
      return true;
    },
  },
};

Authentication

import jwt from "jsonwebtoken";

interface AuthContext {
  user?: { id: string; email: string };
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }): Promise<AuthContext> => {
    const token = req.headers.authorization?.split(" ")[1];
    if (!token) return {};

    try {
      const decoded = jwt.verify(token, "secret");
      return { user: decoded as any };
    } catch {
      return {};
    }
  },
});

// Protect resolvers
const resolvers = {
  Query: {
    me: (parent, args, context: AuthContext) => {
      if (!context.user) throw new Error("Not authenticated");
      return context.user;
    },
  },
};

Query Examples

# Get specific fields
query GetUser {
  user(id: "1") {
    id
    name
    email
  }
}

# Get nested data
query GetUserWithPosts {
  user(id: "1") {
    id
    name
    posts {
      id
      title
    }
  }
}

# Multiple queries
query MultiQuery {
  alice: user(id: "1") {
    name
  }
  bob: user(id: "2") {
    name
  }
}

# Variables
query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
}

Directives

# Skip field conditionally
query GetUser($includeEmail: Boolean!) {
  user(id: "1") {
    name
    email @include(if: $includeEmail)
  }
}

# Custom directive
directive @auth(role: String!) on FIELD_DEFINITION

type Query {
  adminData: String! @auth(role: "admin")
}

Error Handling

import { GraphQLError } from "graphql";

const resolvers = {
  Query: {
    user: async (parent, args) => {
      const user = await getUser(args.id);
      if (!user) {
        throw new GraphQLError("User not found", {
          extensions: { code: "NOT_FOUND" },
        });
      }
      return user;
    },
  },
};

FAQ

Q: When should I use GraphQL vs REST? A: GraphQL for flexible queries. REST for simple, predictable APIs. GraphQL shines with complex data relationships.

Q: What's the performance impact of GraphQL? A: GraphQL adds overhead. REST is faster for simple cases. But GraphQL prevents over-fetching which saves bandwidth.

Q: How do I handle file uploads in GraphQL? A: Use GraphQL Upload scalar type and handle multipart form data.


GraphQL solves REST's shortcomings by letting clients specify exactly what data they need. For complex applications with many data relationships, GraphQL is powerful. For simple APIs, REST remains simpler.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro