Let's try GraphQL!

Understanding GraphQL Core Concepts

By Hank Kim

Let's try GraphQL!

Understanding GraphQL Core Concepts

GraphQL: A Smarter Way to Fetch Data

What Is GraphQL?

GraphQL is a data query language created by Facebook that makes fetching data from servers more efficient. Often shortened to GQL, it’s not tied to any specific database or programming language - it’s simply a better way to ask for data.

Think of it like ordering at a restaurant. With traditional REST APIs, you get a fixed menu where each dish (endpoint) comes with predetermined sides. With GraphQL, you can order exactly what you want: “I’ll take the chicken, but hold the vegetables, add extra rice, and give me the sauce on the side.”

GraphQL Architecture

The flow is straightforward: your client (often using Apollo Client) writes a query describing exactly what data it needs, sends it to a GraphQL server, which then fetches the required data from your database and returns only what was requested.

Server Side: One Endpoint to Rule Them All

Unlike REST APIs where you create multiple endpoints for different resources, GraphQL servers expose a single endpoint that handles all data requests.

Here’s a basic GraphQL server setup:

const express = require("express");
const colors = require("colors");
require("dotenv").config();
const cors = require("cors");
const { graphqlHTTP } = require("express-graphql");
const schema = require("./schema/schema");
const connectDB = require("./config/db");
const port = process.env.PORT || 5001;
const app = express();

connectDB();
app.use(cors());
app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    graphiql: process.env.NODE_ENV === "development",
  })
);

app.listen(port, () => {
  console.log("server running");
});

The graphqlHTTP middleware takes your schema and creates that single endpoint. The graphiql option enables a development tool - visit localhost:5000/graphql and you get a browser-based query explorer.

Queries vs Mutations: The Two Types of Requests

GraphQL operations come in two flavors, similar to React Query’s useQuery and useMutation:

  • Queries: For reading data (like GET requests)
  • Mutations: For modifying data (like POST, PUT, DELETE)
const RootQuery = new GraphQLObjectType({
  name: "RootQueryType",
  fields: {
    clients: {
      type: new GraphQLList(ClientType),
      resolve(parents, args) {
        return Client.find();
      },
    },
    client: {
      type: ClientType,
      args: { id: { type: GraphQLID } },
      resolve(parent, args) {
        return Client.findById(args.id);
      },
    },
  },
});

const mutation = new GraphQLObjectType({
  name: "Mutation",
  fields: {
    addClient: {
      type: ClientType,
      args: {
        name: { type: GraphQLNonNull(GraphQLString) },
        email: { type: GraphQLNonNull(GraphQLString) },
        phone: { type: GraphQLNonNull(GraphQLString) },
      },
      resolve(parent, args) {
        const client = new Client({
          name: args.name,
          email: args.email,
          phone: args.phone,
        });
        return client.save();
      },
    },
  },
});

module.exports = new GraphQLSchema({
  query: RootQuery,
  mutation,
});

Each field has a resolve function (called a resolver) that defines how to fetch or create the data. These resolvers are where you connect to your database, call other APIs, or perform any business logic.

Type Safety: Defining Your Data Shape

GraphQL enforces type safety by requiring you to define the structure of your data:

const ClientType = new GraphQLObjectType({
  name: "Client",
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: GraphQLString },
    email: { type: GraphQLString },
    phone: { type: GraphQLString },
  }),
});

These type definitions serve as both documentation and validation. If your resolver tries to return data that doesn’t match the type, GraphQL will throw an error.

Client Side: Ask for What You Need

From the client’s perspective, GraphQL is refreshingly simple: send queries to one endpoint and get back exactly the data you requested.

Most React apps use Apollo Client, which provides similar functionality to React Query - data fetching, caching, loading states, and error handling - but specifically designed for GraphQL.

Query Example: Fetching Data

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

const GET_CLIENTS = gql`
  query getClients {
    clients {
      id
      name
      email
      phone
    }
  }
`;

function Clients() {
  const { loading, error, data } = useQuery(GET_CLIENTS);

  if (loading) return <Spinner />;
  if (error) return <p>Something went wrong!</p>;

  return (
    <table className="table table-hover mt-3">
      <tbody>
        {data.clients.map((client) => (
          <ClientRow key={client.id} client={client} />
        ))}
      </tbody>
    </table>
  );
}

Notice how the query specifies exactly which fields we want. If we only needed name and email, we could remove id and phone from the query, and the server wouldn’t fetch or send that data.

Mutation Example: Modifying Data

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

export const ADD_CLIENT = gql`
  mutation addClient($name: String!, $email: String!, $phone: String!) {
    addClient(name: $name, email: $email, phone: $phone) {
      id
      name
      email
      phone
    }
  }
`;

const [addClient] = useMutation(ADD_CLIENT, {
  variables: { name, email, phone },
  refetchQueries: [{ query: GET_CLIENTS }],
});

const onSubmit = (e) => {
  e.preventDefault();

  if (name === "" || email === "" || phone === "") {
    return alert("Please fill in all fields");
  }

  addClient(name, email, phone);

  setName("");
  setEmail("");
  setPhone("");
};

The refetchQueries option tells Apollo to refresh specific queries after the mutation completes, ensuring your UI stays in sync with the server.

The Real-World Benefits

1. No More API Versioning Headaches

With REST APIs, adding a new field often means creating a new version (/api/v2/users). With GraphQL, you just add the field to your schema. Old queries continue working unchanged, while new queries can request the additional data.

2. Efficient Data Loading

Consider a social media feed that shows posts with author names and profile pictures. With REST:

// Get posts
fetch("/api/posts")
  // Then for each post, get author details
  .then((posts) => {
    posts.forEach((post) => {
      fetch(`/api/users/${post.authorId}`).then((author) => {
        // Now you can show author name and picture
      });
    });
  });

This creates the notorious N+1 query problem. With GraphQL:

query getFeed {
  posts {
    id
    title
    content
    author {
      name
      profilePicture
    }
  }
}

One request, all the data you need.

3. Frontend-Backend Collaboration

GraphQL acts as a contract between frontend and backend teams. Once they agree on the schema, both teams can work independently. Frontend developers can even mock GraphQL responses while backend developers implement the resolvers.

The Limitations

GraphQL isn’t perfect for every situation:

1. Learning Curve

Setting up GraphQL requires defining schemas, understanding resolvers, and learning new concepts. For simple CRUD applications, REST might be more straightforward.

2. File Uploads

GraphQL doesn’t handle file uploads natively. You’ll need additional solutions or libraries for handling multipart form data.

3. Caching Complexity

REST APIs benefit from HTTP caching mechanisms. With GraphQL’s single endpoint and varied queries, caching becomes more complex, though tools like Apollo help manage this.

The Bottom Line

GraphQL shines when you’re building applications with complex data requirements, multiple client types (web, mobile, desktop), or teams that want to iterate quickly without constantly coordinating API changes.

It’s not about replacing REST entirely - it’s about choosing the right tool for your specific needs. For applications where data efficiency, developer experience, and rapid iteration matter more than simplicity, GraphQL offers a compelling alternative.

The next time you find yourself making multiple API calls to display a single screen, or waiting for backend changes to add one field to your UI, remember there’s a query language designed to solve exactly those problems.