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
/graphqlURL - 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: '[email protected]' },
{ id: '2', name: 'Jane', email: '[email protected]' },
];
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
Related reading
- 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.