Skip to main content

Command Palette

Search for a command to run...

Beginner’s Guide to GraphQL with Apollo: From Server to Client

A complete beginner guide explaining how GraphQL APIs work, how to build them with Apollo Server, connect them with Apollo Client

Updated
7 min read
Beginner’s Guide to GraphQL with Apollo: From Server to Client
R

Engineer @Ciena | Software Engineering | Full Stack Development | Typescript , Java, React Node | DSA LeetCode (600+) | System Design |

Modern applications aren’t simple anymore — they have dashboards, analytics, notifications, and nested data everywhere. REST APIs start feeling clunky as your frontend grows more dynamic. That’s where GraphQL shines — a modern query language that gives the client exactly what it needs, in one request.

What Is GraphQL?

GraphQL is a query language for APIs and a runtime to execute those queries. You can think of it as a flexible and intelligent middleman between your frontend and backend.

Why do we even need GraphQL when REST already works?

The Problem with REST APIs

Traditional REST APIs organise data around endpoints. For example, a blog API might look like this:

// Each endpoint returns a fixed data structure, often too much or too little information.

GET /users/1
GET /users/1/posts
GET /posts/5/comments

This leads to two big pain points:

  • Overfetching

    You get more data than you need.

    Eg: You just need a user’s name, but /users/1 returns their entire profile, preferences, posts, and settings. Wasted bandwidth. Slower load times.

  • Underfetching

    You get less data than you need — forcing multiple API calls.

    Eg: To display a user’s name and their latest posts, you might have to call both /users/1 and /users/1/posts. More round trips. More complexity.

How GraphQL Fixes This

GraphQL flips the model:

  • You don’t hit multiple endpoints.

  • You hit one single endpoint => /graphql.

  • You describe exactly what data you need.

Example:

query {
  user(id: 1) {
    name
    posts {
      title
      comments {
        text
      }
    }
  }
}

This query says:

“Give me the user’s name, their posts, and each post’s comments, but only these fields.”

Server response:

{
  "data": {
    "user": {
      "name": "Ram",
      "posts": [
        {
          "title": "Learning GraphQL",
          "comments": [{ "text": "Nice article!" }]
        }
      ]
    }
  }
}

No overfetching
No underfetching
No multiple requests


GraphQL vs REST

ConceptRESTGraphQL
EndpointMultiple (/users, /posts, /comments)Single (/graphql)
Data FetchingFixed structureClient decides structure
OverfetchingCommonAvoided
UnderfetchingCommonAvoided
Real-TimeNeeds extra setupBuilt-in via Subscriptions

How GraphQL Works Internally

GraphQL works on three core concepts:

  1. Schema - Defines what data and operations exist.

  2. Resolver - Functions that tell how to fetch that data.

  3. Query / Mutation / Subscription - The operations clients perform.

The Schema: Defining Your Graph

The schema defines what’s possible - what you can ask for and what you’ll get back.
This gives GraphQL a strong type system, meaning:

  • You know every field’s data type.

  • You can validate queries before running them.

  • You can use auto-complete and documentation tools easily.

type User {
  id: ID!
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  text: String!
}

Example Flow:

Flow Summary:
Client Query → Schema Validation → Resolver Function → Data Fetch → Formatted Response

Step 1: Client sends a query:

query {
  todos {
    id
    title
    completed
  }
}

Step 2: GraphQL server matches it to a resolver function:

const resolvers = {
  Query: {
    todos: () => getAllTodosFromDB()
  }
}

Step 3: Resolver fetches data and returns it.

Step 4: GraphQL formats the result according to the schema and sends it back.

There are many GraphQL server libraries, but Apollo Server is by far the most popular.

It provides:

  • A ready-to-use HTTP layer for /graphql

  • Schema definition tools

  • Middleware integrations

  • Developer tools like GraphQL Playground

Common Apollo Functions & Concepts:

Function / ConceptDescription
ApolloServerThe main GraphQL server instance
typeDefsThe schema definition (string or SDL)
resolversObject mapping queries/mutations to functions
server.start()Starts the Apollo Server
expressMiddleware(server)Connects Apollo with Express
contextShared object across resolvers (for auth, db, etc.)

GraphQL Architecture Overview

GraphQL has three main layers:

  1. Schema Definition
    Defines the shape of your data and the operations available.

  2. Resolvers
    Actual functions that run to fetch the data from your database or APIs.

  3. Client Query
    The query that requests specific data fields.

Schema Example

const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

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

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

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

Why:
The schema tells GraphQL what the data looks like — like a contract between client and server.


Resolver Example

const resolvers = {
  Query: {
    users: async () => await UserModel.find(),
    user: async (_, { id }) => await UserModel.findById(id),
  },
  Mutation: {
    createUser: async (_, { name, email }) => {
      const user = new UserModel({ name, email });
      return user.save();
    },
  },
  User: {
    posts: async (parent) => await PostModel.find({ userId: parent.id }),
  },
};

Why:
Resolvers are functions that tell GraphQL how to fetch the data for each field.


Setting up the GraphQL Server

Let’s use Apollo Server (v4) with Express.js.

Install dependencies

npm install @apollo/server graphql express body-parser cors

Server Setup

import express from "express";
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import bodyParser from "body-parser";
import cors from "cors";

const app = express();
app.use(cors(), bodyParser.json());

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

await server.start();

app.use("/graphql", expressMiddleware(server));

app.listen(4000, () => console.log("Server running on http://localhost:4000/graphql"));

Why:

  • ApolloServer runs your schema and resolvers.

  • /graphql acts as the single endpoint for all queries and mutations.

  • You can test your queries in Apollo Sandbox or GraphQL Playground.

Client Setup with Apollo Client

Let’s now connect the frontend to this server.

  • Install dependencies

npm install @apollo/client graphql
  • Apollo Client Setup

import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";

const client = new ApolloClient({
  link: new HttpLink({
    uri: "http://localhost:4000/graphql",
  }),
  cache: new InMemoryCache(),
});

export default client;

What’s happening here:

ComponentPurpose
ApolloClientThe main class that connects to the GraphQL server
HttpLinkDefines how queries/mutations are sent over HTTP
InMemoryCacheStores query results locally for instant re-use and caching

Example Queries & Mutations (Client Side)

Query Example

import { gql, useQuery } from "@apollo/client";

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

export default function UserList() {
  const { data, loading, error } = useQuery(GET_USERS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.users.map((u) => (
        <li key={u.id}>{u.name} — {u.email}</li>
      ))}
    </ul>
  );
}

How this works:

  1. React component loads → executes the useQuery hook.

  2. Apollo Client sends the query to /graphql.

  3. Apollo Server matches it with the resolver → fetches DB data.

  4. Response is cached and returned to the client.


Mutation Example

import { gql, useMutation } from "@apollo/client";

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

export default function AddUser() {
  const [createUser, { data, loading }] = useMutation(CREATE_USER);

  const handleAdd = () => {
    createUser({ variables: { name: "John Doe", email: "john@example.com" } });
  };

  return (
    <div>
      <button onClick={handleAdd}>Add User</button>
      {loading && <p>Creating...</p>}
      {data && <p>Created user: {data.createUser.name}</p>}
    </div>
  );
}

How this works:

  1. When button clicked → mutation executes.

  2. Apollo Client sends mutation request.

  3. Server runs resolver logic, updates DB, returns new user.

  4. Apollo caches response automatically.

Thank you for visiting again.