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.”
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.