Quick Summary: This beginner-friendly guide explains what GraphQL is and why it was created, compares it with REST using a real screen example, and shows how to read data with queries and change data with mutations. It then walks through every core GraphQL type (scalars, objects, lists, non-null, enums, input types, interfaces and unions) with a small example app you can follow along with.
Introduction
Think about two ways of ordering lunch. In the first, the restaurant sells fixed combos. You only want a burger and a drink, but the combo also comes with fries, a dessert, and a sauce you will never use. If you want something extra, you place another order.
In the second, you tell the waiter exactly what you want, and one tray arrives with exactly that.
REST APIs often feel like the first way. GraphQL is the second. It lets an app ask a server for the exact data it needs, across related items, in a single request.
Here we cover:
- What GraphQL is and why it exists
- How it differs from REST
- How to read data (queries) and change data (mutations)
- The types GraphQL uses to describe data: scalars, objects, lists, and more
All examples use one small app: an online store where a Seller sells a Product, and customers leave Reviews.
What Is GraphQL?
GraphQL is two things in one:
- A query language. A way for the client to describe the data it wants.
- A runtime. The server-side engine that checks that request against a typed schema and returns the result.
Three ideas sit at the heart of it:
- The client chooses the fields. You ask for exactly what a screen needs, nothing more.
- The schema is the contract. It lists every type, field and relationship the API offers.
- Usually one endpoint. Most requests go to a single URL, commonly
GraphQL began inside Facebook (now Meta) to serve its mobile apps. It was open-sourced in 2015 and is now looked after by the GraphQL Foundation.
Two things it is not:
- It is not a database. It sits between your app and your data sources (databases, other APIs, services).
- It is not a single product. GraphQL is a specification, and many tools implement it, such as Apollo Server, Spring for GraphQL, Netflix DGS, and Hot Chocolate.
Why Did GraphQL Appear?
The problem was never that REST is bad. The problem was getting the right data efficiently as apps grew more complex. Three pain points kept showing up.
- Over-fetching. The API sends more than the screen needs. A profile badge only needs a name and an avatar, but the endpoint returns the whole user object: address, phone, preferences, history.
- Under-fetching. One endpoint does not have everything the screen needs, so the app makes several calls and stitches the answers together.
- Changing UI needs. The web app, the mobile app, and a smart TV all want different fields. Every time a screen changes, someone has to adjust an endpoint.
GraphQL's answer is to let the client describe the shape of the response. The mindset changes from "which endpoint gives me this object?" to "what data does this screen need?"
GraphQL vs REST: What's the Difference?
Let's build one screen: a Product details page. It shows the product name, price, and status. It also shows the seller's name and a list of customer reviews.
With REST
The data lives in different places, so the app makes several calls:
GET /products/42 -> product details (plus many fields the page never shows)
GET /products/42/reviews -> the reviews
GET /sellers/7 -> the seller's nameThree round trips, and each response carries extra data the screen throws away.
With GraphQL
The app sends one request to one endpoint:
query {
product(id: "42") {
name
price
status
seller { name }
reviews { comment }
}
}The server answers with exactly those fields and nothing else.
The main differences
| REST | GraphQL | |
| Endpoints | Many resource endpoints | Usually one /graphql endpoint |
| Response shape | Decided by the server | Chosen by the client |
| Nested data | Often needs multiple calls | Related data can come back together |
| Versioning | Often /v1, /v2 | Add new fields and deprecate old ones |
| Contract | OpenAPI or similar tooling | The typed schema itself |
| Caching | Works naturally with HTTP and CDNs | Harder, because requests usually go to one URL |
| File upload | Native and simple | Not part of the core spec, so usually kept on REST |
Important: this is not "REST is old, GraphQL is the future." REST is often simpler for stable resources, public APIs, file transfers, and anything that benefits from HTTP caching. GraphQL shines when screens need nested data or different clients need different shapes. Many teams use both side by side.
The GraphQL Schema: Your API's Contract
Before a client can send a single query, the server publishes a schema. It is written in the Schema Definition Language (SDL) and describes everything that can be asked for.
Here is the schema for our online store:
enum ProductStatus {
IN_STOCK
OUT_OF_STOCK
}
type Seller {
id: ID!
name: String!
}
type Review {
id: ID!
comment: String!
rating: Int!
}
type Product {
id: ID!
name: String!
price: Float!
status: ProductStatus!
stockCount: Int
seller: Seller!
reviews: [Review!]!
}
input AddProductInput {
name: String!
price: Float!
stockCount: Int
}
type Query {
product(id: ID!): Product
products: [Product!]!
}
type Mutation {
addProduct(input: AddProductInput!): Product!
}So the schema works like a menu: if something is not on it, the client cannot ask for it.
The server also checks every request against the schema before running any code. Ask for a field that does not exist, or pass the wrong type, and the request is rejected straight away.
The GraphQL Type System
Everything in a schema has a type. This is what lets GraphQL check requests before running them, and what powers autocomplete and code generation in tools. You only need a handful of types to get started.
1. Scalar types: single values
Scalars are the leaves of a query, the actual values. GraphQL has five built-in types:
| Scalar | What it holds | Example value |
Int | A signed 32-bit whole number | 25 |
Float | A decimal number | 499.99 |
String | Text | "Running Shoes" |
Boolean | true or false | true |
ID | A unique identifier, sent as text | "42" |
Servers can also define custom scalars for things like dates and emails, for example, a DateTime scalar.
2. Object types: structured data
An object type is a named set of fields, and each field has its own type. It is how you describe a real-world thing such as a Product or a Seller.
type Product {
id: ID!
name: String!
seller: Seller!
}Fields can point to other object types, like seller above. That is how GraphQL builds a graph of related data and lets you walk from a product to its seller to anything else linked to that seller in a single query.
3. Lists: collections
Square brackets turn any type into a list.
reviews: [Review!]!This reads as "a list of Review objects." In a query, you pick the fields you want for each item, and you get back an array.
4. Non-null: the exclamation mark
By default, any field in GraphQL can be null. Adding ! is a promise that the value will always be there.
- name: String!
- stockCount: Int
The ! becomes interesting when combined with lists. These four look similar but mean different things:
| Written as | Can the list itself be null? | Can items inside be null? |
[String] | Yes | Yes |
[String!] | Yes | No |
[String]! | No | Yes |
[String!]! | No | No |
The last one, [String!]!, is the most common and the easiest to work with.
5. Enum types: a fixed set of choices
An enum limits a field to a small list of allowed values.
enum ProductStatus {
IN_STOCK
OUT_OF_STOCK
DISCONTINUED
}6. Input types: structured arguments
Object types describe what the server returns. When a client needs to send a structured object, such as the details of a new product, it uses an input type (used mainly in mutations).
input AddProductInput {
name: String!
price: Float!
stockCount: Int
}7. Interfaces and unions
An interface is a set of fields that several types promise to share:
interface Item {
id: ID!
name: String!
}
type Gadget implements Item {
id: ID!
name: String!
warrantyMonths: Int!
}A union says "this field can be one of several types", without needing shared fields. For example, a search result could be either a Product or a Seller.
8. The root types: Query, Mutation, Subscription
These special object types are the entry points to your API. Every query starts at a field on Query, and every mutation starts at a field on Mutation.
type Query {
product(id: ID!): Product
products: [Product!]!
}
type Mutation {
addProduct(input: AddProductInput!): Product!
}If it is not reachable from one of these roots, a client cannot get to it.
Reading Data: The GraphQL Query
A query is a read-only operation. The client describes the exact response shape it wants.
query GetProduct($id: ID!) {
product(id: $id) {
name
price
status
seller { name }
reviews { comment }
}
}Variables are sent separately as JSON:
{ "id": "42" }The response:
{
"data": {
"product": {
"name": "Running Shoes",
"price": 2499.5,
"status": "IN_STOCK",
"seller": { "name": "Urban Sports" },
"reviews": [
{ "comment": "Very comfortable" },
{ "comment": "Good value for money" }
]
}
}
}Notice: the response has the same shape as the query. Ask for five things, get five things. Ask for only the name, and you get only the name.
How the request travels
Over HTTP, the app sends a POST to the GraphQL endpoint with a small JSON body:
POST /graphql
{
"query": "query GetProduct($id: ID!) { product(id: $id) { name } }",
"variables": { "id": "42" }
}Writing Data: The GraphQL Mutation
A mutation changes data on the server: create, update or delete. It looks like a query, but it starts with the word mutation.
mutation AddProduct($input: AddProductInput!) {
addProduct(input: $input) {
id
name
status
}
}Variables:
{
"input": {
"name": "Wireless Mouse",
"price": 799.0,
"stockCount": 25
}
}Response:
{
"data": {
"addProduct": {
"id": "43",
"name": "Wireless Mouse",
"status": "IN_STOCK"
}
}
}This shows a nice feature: a mutation can return data. After adding the product, we asked for its new ID, name, and status in the same request. The app does not need a second call to find out what happened.
GraphQL Query vs Mutation
| Query | Mutation | |
| Purpose | Read data | Change data (create, update, delete) |
| Side effects | None; it should only read | Changes server state |
| Keyword | query | mutation |
| Execution | Fields may be resolved in parallel | Top-level mutation fields run one after another |
| Example | product(id: 42) | addProduct(input: ...) |
There is also a third operation type, the subscription. It opens a long-lived connection (usually a WebSocket) so the server can push live events, such as an order tracking page that updates as a parcel moves from packed to shipped.
Putting It All Together
Here is how the pieces connect for one simple flow.
- Add a product with a mutation. It uses the input type
- The server stores it and returns the fields you asked for, including the enum status
- Read it back with a query: pass the
- The response mirrors your query, and the schema guarantees every value matches its declared type.
Conclusion
GraphQL is a simple idea: the client asks for exactly the fields it needs, the schema defines what is allowed, and the server answers in the same shape. You have now seen what it is, why it appeared, how it differs from REST, how queries read, and mutations write, and the types that make it all predictable.
If you remember only five things:
- The client asks for the fields it needs.
- The schema defines what is allowed.
- Query reads. Mutation changes.
- Nested data can come back in one request.
- The response has the same shape as the query.



