GraphQL APIs
Reviewed & published by Brayan K
GraphQL is a query language for APIs where the client asks for exactly the fields it needs from a single, strongly typed endpoint, and the server answers using one resolver function per field.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll compare GraphQL with REST, write a schema with types, queries, and mutations, understand how resolvers fetch each field, see the core query executor in action, and wire it all together with Apollo Server.
What You'll Learn in This Lesson
- How GraphQL differs from REST (over- and under-fetching)
- Writing a schema with types and the ! non-null marker
- The difference between a query and a mutation
- How resolvers produce the value for each field
- How a query selects exactly the fields it wants
- Running a real GraphQL API with Apollo Server
1️⃣ GraphQL vs REST: the Schema
A GraphQL API starts with a schema written in the Schema Definition Language (SDL). It declares your types (like User), the readable fields under type Query, and the write operations under type Mutation. A ! marks a field as non-null (required).
Where REST might force you to call /users/1 and get the whole record, a GraphQL query names just the fields it wants — so the client decides the response shape, avoiding both over-fetching and under-fetching.
# A GraphQL SCHEMA describes your data's types and what can be queried.
# This is the GraphQL Schema Definition Language (SDL), not JavaScript.
type User {
id: ID! # ! means non-null (required)
name: String!
email: String!
role: String!
}
type Query {
user(id: ID!): User # fetch one user by id
users: [User!]! # fetch the whole list
}
type Mutation {
addUser(name: String!, email: String!): User
}
# A client QUERY asks for exactly the fields it wants — no more, no less:
#
# query {
# user(id: 1) { name role } # note: email is NOT requested
# }2️⃣ Resolvers: How Fields Get Their Values
The schema says what exists; resolvers say how to fetch it. A resolver is one function per field that returns its value — from a database, another API, or an array. GraphQL then keeps only the fields the client asked for. The runnable executor below models exactly that: it resolves the data, then picks just the requested fields.
3️⃣ The Real Thing: Apollo Server
In production you hand your typeDefs (the SDL) and your resolvers to Apollo Server, which parses each incoming query, validates it against the schema, calls the right resolvers, and assembles the response — plus a sandbox UI for exploring your API. Notice resolvers are grouped under Query and Mutation, and each receives (parent, args, context).
// The real thing with Apollo Server.
// Install once: npm install @apollo/server graphql
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
// 1) The schema (typeDefs) — same SDL as above.
const typeDefs = `#graphql
type User { id: ID!, name: String!, email: String!, role: String! }
type Query { user(id: ID!): User, users: [User!]! }
type Mutation { addUser(name: String!, email: String!): User }
`;
const users = [{ id: '1', name: 'Ada', email: 'ada@x.com', role: 'admin' }];
// 2) Resolvers — one function per field, grouped by Query/Mutation.
const resolvers = {
Query: {
user: (_parent, { id }) => users.find((u) => u.id === id),
users: () => users,
},
Mutation: {
addUser: (_parent, { name, email }) => {
const user = { id: String(users.length + 1), name, email, role: 'user' };
users.push(user);
return user;
},
},
};
// 3) Start the server — Apollo wires schema + resolvers together.
const server = new ApolloServer({ typeDefs, resolvers });
startStandaloneServer(server, { listen: { port: 4000 } })
.then(({ url }) => console.log(`Ready at ${url}`));Your turn. The executor below works once you fill in the field list marked ___. Follow the 👉 hint, then run it.
// 🎯 YOUR TURN — replace each ___ then run it.
const products = [
{ id: 1, title: 'Keyboard', price: 80, stock: 5 },
{ id: 2, title: 'Mouse', price: 25, stock: 0 },
];
const resolvers = {
products: () => products,
};
function execute(rootField, fields) {
const data = resolvers[rootField]();
return data.map((obj) => Object.fromEntries(fields.map((f) => [f, obj[f]])));
}
// Ask for only title + price of every product (fill in the field list):
console.log(execute('products', [___, ___])); // 👉 'title', 'price'
// ✅ Expected output:
// [ { title: 'Keyboard', price: 80 }, { title: 'Mouse', price: 25 } ]Mini-Challenge: A Query + a Mutation
No blanks — just a brief and an outline. Build a resolver map with a Query.todos reader and a Mutation.addTodo writer, then show the list growing. Run it and compare with the example output.
// 🎯 MINI-CHALLENGE: a resolver map with a query AND a mutation
// 1. Keep todos in db = { todos: [{ id: 1, text: 'Learn GraphQL', done: false }] }.
// 2. resolvers.Query.todos() returns db.todos.
// 3. resolvers.Mutation.addTodo({ text }) pushes a new { id, text, done:false }
// and returns it.
// 4. Print the list before, the added todo, and the list after.
//
// ✅ Example output:
// Before: [ { id: 1, text: 'Learn GraphQL', done: false } ]
// Added: { id: 2, text: 'Ship app', done: false }
// After: [ ...two todos... ]
const db = { todos: [{ id: 1, text: 'Learn GraphQL', done: false }] };
// your code hereCommon Errors (and the fix)
- ❌ "Cannot query field X on type Y" — you asked for a field that isn't in the schema. ✅ Add it to the type's SDL and give it a resolver.
- ❌ A field returns null unexpectedly — its resolver returned nothing, or the parent object used a different key. ✅ Check the resolver's return value and the parent's property names.
- ❌ Mutations that should write don't persist — you defined the field under Query instead of Mutation, or forgot to actually save. ✅ Put writes under Mutation and perform the database/array change inside the resolver.
- ❌ One huge nested query brings the server to its knees — clients can request deeply nested data. ✅ Add query depth/complexity limits and use DataLoader to batch database calls.
- ❌ Non-null (!) field resolved to null throws — GraphQL errors when a required field is missing. ✅ Ensure those resolvers always return a value, or drop the !.
Pro Tips
- 💡 Design the schema first: agree on types and fields before writing resolvers — the schema is the contract between front end and back end.
- 💡 Use the context for auth and the DB: put the logged-in user and database handle in Apollo's context so every resolver can reach them.
- 💡 Batch with DataLoader: it collapses many per-item database calls into one, fixing the classic N+1 query problem.
📋 Quick Reference — GraphQL Building Blocks
| Concept | Detail |
|---|---|
| Define a type | type User { id: ID! } |
| Required field | name: String! |
| Read data | type Query |
| Write data | type Mutation |
| Resolver signature | (parent, args, context) |
| Run a server | new ApolloServer(...) |
| Fix N+1 | DataLoader |
🎉 Lesson Complete!
- ✅ GraphQL lets the client request exactly the fields it needs from one endpoint
- ✅ The schema (SDL) declares types, queries, mutations, and non-null (!) fields
- ✅ Resolvers are one function per field that fetch its value
- ✅ Queries read; mutations write
- ✅ Apollo Server wires schema + resolvers into a running API
- ✅ Watch out for deep queries and the N+1 problem — limit depth and use DataLoader
Practice quiz
What is a key advantage of GraphQL over a typical REST endpoint?
- It requires no server
- The client asks for exactly the fields it wants
- It only returns XML
- It cannot be queried
Answer: The client asks for exactly the fields it wants. A GraphQL client requests precisely the fields it needs, no more, no less.
In GraphQL SDL, what does the ! after a type mean, as in String!?
- The field is non-null (required)
- The field is deprecated
- The field is a list
- The field is optional
Answer: The field is non-null (required). ! marks a field as non-null, meaning it must always have a value.
What is a resolver in GraphQL?
- A database table
- A caching layer
- A function that returns the value for a field
- A type definition
Answer: A function that returns the value for a field. Each field has a resolver function that produces its value.
Which special type holds read operations in a GraphQL schema?
- Mutation
- Subscription
- Schema
- Query
Answer: Query. The Query type defines the fields clients can read/fetch.
Which type is used for operations that CHANGE data, like addUser?
- Mutation
- Query
- Fragment
- Scalar
Answer: Mutation. The Mutation type defines write operations such as adding a user.
What does the type notation [User!]! describe?
- A single optional user
- A non-null list of non-null Users
- A string of users
- An empty list
Answer: A non-null list of non-null Users. [User!]! is a required list whose every element is a non-null User.
Which library is shown for building a real GraphQL server in the lesson?
- Express GraphQL Classic
- GraphiQL only
- Apollo Server (@apollo/server)
- Mongoose
Answer: Apollo Server (@apollo/server). The lesson uses @apollo/server with startStandaloneServer.
How does a client avoid over-fetching with GraphQL?
- It downloads the whole database
- The server always sends every field
- It uses multiple endpoints
- It lists only the fields it needs in the query
Answer: It lists only the fields it needs in the query. By naming just the fields it wants, the client gets exactly that data.
In Apollo Server, how are resolvers organized?
- Grouped by type, e.g. Query and Mutation
- In a single flat array
- Inside the database
- As URL routes
Answer: Grouped by type, e.g. Query and Mutation. Resolvers are grouped under Query, Mutation, etc., one function per field.
What is the ID type's role in the User schema?
- It stores an image
- It is a unique identifier for the object
- It is the user's password
- It is a timestamp
Answer: It is a unique identifier for the object. ID is a scalar serving as a unique identifier for an object.
Continue this course
- Previous: WebSockets (ws & Socket.IO)
- Next: SQL Databases with Sequelize — Model and query relational data with the Sequelize ORM
- Quick reference: Node.js cheat sheet
Frequently asked questions
What is the main difference between GraphQL and REST?
With REST, the server decides the shape of each endpoint's response, so you often get more data than you need (over-fetching) or have to call several endpoints to assemble one screen (under-fetching). With GraphQL, there's typically a single endpoint and the client sends a query describing exactly which fields it wants — including nested, related data — and gets back precisely that, in one round trip. The trade-offs: REST is simpler, caches naturally over HTTP, and is easy to debug; GraphQL gives clients flexibility and a strongly typed schema but needs more server setup and careful handling of caching and expensive queries.
What is a resolver in GraphQL?
A resolver is the function that produces the value for a single field in your schema. When a query asks for a field, GraphQL calls that field's resolver, passing it the parent object, the arguments, and a shared context (often holding the database connection or the authenticated user). For example, a Query.user resolver looks up a user by id; a User.posts resolver fetches that user's posts. Resolvers are where your real data-fetching lives — calling a database, another API, or in our runnable example, picking values out of an in-memory array. The schema says what's available; resolvers say how to get it.
What is the difference between a query and a mutation?
By convention, a query READS data and should have no side effects, while a mutation WRITES data — creating, updating, or deleting it. They look almost identical and both run through resolvers, but separating them communicates intent and lets tooling treat them differently (for instance, mutations run one at a time in series to avoid race conditions, whereas a query's fields can resolve in parallel). In SDL you declare readable fields under type Query and write operations under type Mutation, then provide a resolver for each — exactly the Query/Mutation split shown in this lesson's runnable challenge.
What is Apollo Server and do I need it?
Apollo Server is the most popular Node library for running a GraphQL API. You give it your schema (typeDefs in SDL) and your resolvers, and it handles parsing incoming queries, validating them against the schema, calling the right resolvers, assembling the response, and reporting errors — plus a built-in sandbox UI for exploring your API. You don't strictly need it (the lower-level graphql package or alternatives like GraphQL Yoga also work), but Apollo's standalone server gets you from schema to a running endpoint in a few lines, which is why it's the common starting point. The tiny executor in this lesson shows the core idea Apollo automates for you.