The Complete GraphQL Guide | Schema, Resolver, Apollo, Mutation, Subscription

The Complete GraphQL Guide | Schema, Resolver, Apollo, Mutation, Subscription

What this post covers

This is a complete guide to building efficient APIs with GraphQL. It walks through schema definition, resolvers, queries, mutations, subscriptions, and Apollo Server and Apollo Client with practical examples.

From the field: Migrating from REST to GraphQL cut API call volume by about 70% and roughly doubled mobile app performance in one project.

Introduction: “Our REST API feels inefficient”

Real-world scenarios

Scenario 1: Over-fetching

You receive fields you never use. GraphQL lets clients request only the fields they need. Scenario 2: Under-fetching and multiple round trips

You call several endpoints to stitch related data. GraphQL can fetch it in one request. Scenario 3: Painful API versioning

You maintain /v1, /v2, and so on. GraphQL evolves without explicit versioned URLs.


1. What is GraphQL?

Core characteristics

GraphQL is a query language for APIs. Key benefits:

  • Precise data: request only what you need
  • Single endpoint: typically one /graphql URL
  • Type system: strong schema-driven typing
  • Real time: subscriptions for live updates
  • Self-describing: the schema doubles as documentation REST vs GraphQL:
  • REST: three calls (user, posts, comments)
  • GraphQL: one call

2. Apollo Server

Installation

npm install @apollo/server graphql

Basic server

// server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
  }
  type Query {
    users: [User!]!
    user(id: ID!): User
  }
`;
const users = [
  { id: '1', name: 'John', email: 'john@example.com' },
  { id: '2', name: 'Jane', email: 'jane@example.com' },
];
const resolvers = {
  Query: {
    users: () => users,
    user: (_: any, { id }: { id: string }) => 
      users.find(u => u.id === id),
  },
};
const server = new ApolloServer({
  typeDefs,
  resolvers,
});
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});
console.log(`Server ready at ${url}`);

3. Defining the schema

Types

The following defines core object types in the graph: User, Post, and Comment, including how they reference each other (for example, a user’s posts and a post’s author).

type User {
  id: ID!
  name: String!
  email: String!
  age: Int
  posts: [Post!]!
}
type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  createdAt: String!
}
type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

Query

The Query type lists read-only entry points. Arguments such as id, limit, and offset control filtering and pagination.

type Query {
  users: [User!]!
  user(id: ID!): User
  posts(limit: Int, offset: Int): [Post!]!
  post(id: ID!): Post
}

Mutation

The Mutation type defines operations that create, update, or delete data. Return types should reflect what the client needs after the change.

type Mutation {
  createUser(name: String!, email: String!): User!
  updateUser(id: ID!, name: String, email: String): User!
  deleteUser(id: ID!): Boolean!
  
  createPost(title: String!, content: String!, authorId: ID!): Post!
}

Subscription

Subscriptions expose a push-style API over a long-lived connection (often WebSockets) for events such as new posts or new comments.

type Subscription {
  postCreated: Post!
  commentAdded(postId: ID!): Comment!
}

4. Resolvers

Basic resolvers

const resolvers = {
  Query: {
    users: async () => {
      return await db.user.findMany();
    },
    
    user: async (_: any, { id }: { id: string }) => {
      return await db.user.findUnique({ where: { id: parseInt(id) } });
    },
  },
  Mutation: {
    createUser: async (_: any, { name, email }: { name: string; email: string }) => {
      return await db.user.create({
        data: { name, email },
      });
    },
  },
  User: {
    posts: async (parent: any) => {
      return await db.post.findMany({
        where: { authorId: parent.id },
      });
    },
  },
};

5. Apollo Client (React)

Installation

npm install @apollo/client graphql

Setup

// src/lib/apollo.ts
import { ApolloClient, InMemoryCache } from '@apollo/client';
export const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});

Wrap your React tree with ApolloProvider so hooks like useQuery can reach the client instance.

// src/App.tsx
import { ApolloProvider } from '@apollo/client';
import { client } from './lib/apollo';
function App() {
  return (
    <ApolloProvider client={client}>
      <YourApp />
    </ApolloProvider>
  );
}

6. Using queries

useQuery

useQuery runs a query when the component mounts and exposes loading, error, and data. Handle loading and error states before rendering lists.

import { gql, useQuery } from '@apollo/client';
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;
function UsersList() {
  const { loading, error, data } = useQuery(GET_USERS);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return (
    <ul>
      {data.users.map((user) => (
        <li key={user.id}>
          {user.name} ({user.email})
        </li>
      ))}
    </ul>
  );
}

Variables

Pass GraphQL variables from React props or state so the same document can be reused for different IDs or filters.

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;
function UserProfile({ userId }: { userId: string }) {
  const { data } = useQuery(GET_USER, {
    variables: { id: userId },
  });
  return (
    <div>
      <h1>{data?.user.name}</h1>
      <p>{data?.user.email}</p>
      <h2>Posts</h2>
      <ul>
        {data?.user.posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

7. Using mutations

useMutation

useMutation returns a function you call when the user submits a form or triggers an action. Use refetchQueries or cache updates so lists stay in sync after writes.

import { gql, useMutation } from '@apollo/client';
const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;
function CreateUserForm() {
  const [createUser, { loading, error }] = useMutation(CREATE_USER, {
    refetchQueries: [{ query: GET_USERS }],
  });
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    await createUser({
      variables: {
        name: formData.get('name'),
        email: formData.get('email'),
      },
    });
  };
  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

8. Subscriptions

Server

import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
const schema = makeExecutableSchema({ typeDefs, resolvers });
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
});
useServer({ schema }, wsServer);

Client

useSubscription listens for events published by the server. Until the first payload arrives, you usually show a loading or “waiting” state.

import { useSubscription, gql } from '@apollo/client';
const POST_CREATED = gql`
  subscription OnPostCreated {
    postCreated {
      id
      title
      author {
        name
      }
    }
  }
`;
function PostFeed() {
  const { data, loading } = useSubscription(POST_CREATED);
  if (loading) return <p>Waiting for posts...</p>;
  return (
    <div>
      <p>New post: {data?.postCreated.title}</p>
    </div>
  );
}

Job search and interviews

N+1 issues, schema design, and REST trade-offs come up often in API and backend interviews. Pair Tech Interview Preparation Guide with the projects and achievements sections in Developer Job Hunting Practical Tips when you describe service experience on your resume.


Summary and checklist

Key takeaways

  • GraphQL: a query language for APIs
  • Precise data: request only what you need
  • Single endpoint: /graphql
  • Type system: strong schema-driven safety
  • Real time: subscriptions where needed
  • Apollo: the most widely used client and server stack

Implementation checklist

  • Set up Apollo Server
  • Define the schema
  • Implement resolvers
  • Configure Apollo Client
  • Implement queries and mutations
  • Add subscriptions (optional)
  • Deploy

  • The Complete tRPC Guide
  • The Complete NestJS Guide
  • The Complete Prisma Guide

Keywords in this post

GraphQL, API, Apollo, Schema, Resolver, Backend, TypeScript

Frequently asked questions (FAQ)

Q. GraphQL vs REST—which is better?

A. GraphQL fits complex, varying client data needs. REST stays simple and HTTP caching is well understood. Mobile apps with heavy aggregation often benefit from GraphQL; small, stable APIs may stay on REST.

Q. How do I fix the N+1 problem?

A. Use DataLoader (or similar) for batching and per-request caching so related fields do not trigger one query per row.

Q. How does caching work?

A. Apollo Client caches normalized results by default. On the server, add Redis or HTTP caching where appropriate for your traffic pattern.

Q. Is GraphQL production-ready?

A. Yes. Companies such as Facebook, GitHub, and Shopify run GraphQL at scale.