Build a GraphQL API with Apollo Server
Build a GraphQL API with queries, mutations, and a schema from scratch.
Introduction
30 Second Summary
Every app you use fetches data from a server. Traditional APIs return fixed chunks of data whether you need all of it or not. GraphQL flips that model by letting the client ask for exactly the fields it needs in a single request.
In this project, you will build a book collection API from scratch using Apollo Server and Node.js. You will define a schema, write resolvers, and test your queries and mutations in a live explorer.
What You'll Build
A locally running GraphQL API that serves book and author data, accepts queries with arguments, and lets you add new books through mutations.
By the end of this project, you'll have:
- A Node.js project with Apollo Server running locally on port 4000.
- A GraphQL schema with Book and Author types, plus Query and Mutation operations.
- Resolvers that handle fetching, filtering, and creating in-memory data.
- Hands-on experience writing and testing operations in Apollo Sandbox, the built-in GraphQL explorer.
- Secret Mission: Add a delete mutation and author-based filtering to make your API production-realistic.
Are there any prerequisites?
You should be comfortable with basic JavaScript (variables, functions, arrays) and know how to run commands in a terminal. No prior GraphQL or API experience is needed.
Set Up Your Project
Every GraphQL API needs a JavaScript runtime to execute it and a framework to handle the schema and queries.
In this step, you will install Node.js, create your project folder, and install the two packages your server needs: Apollo Server and the GraphQL library.
In this step, get ready to:
- Install and verify Node.js.
- Create and initialize your project with Apollo Server.
- Confirm the project runs without errors.
Install Node.js and open Cursor
Node.js is the JavaScript runtime that powers your server. Apollo Server requires version 20 or higher.
- Check if Node.js is installed by running this command in your terminal:
node --version
✔️ I see version 20 or higher
Your Node.js version is ready. Move on to the next bullet below.
ⓧ I see an older version
You need version 20 or higher for Apollo Server to work correctly.
- Visit nodejs.org/en/download and download the LTS installer for your operating system.
- Run the installer and accept the defaults.
- Close and reopen your terminal, then run node --version again to confirm the update.
ⓧ Command not found
Node.js is not installed on your machine yet.
- Visit nodejs.org/en/download and download the LTS installer for your operating system.
- Run the installer and accept the defaults.
- Close and reopen your terminal, then run node --version again to confirm it prints a version number.
- Verify that npm (the Node.js package manager) is also available by running:
npm --version
You should see a version number printed. npm comes bundled with Node.js, so if Node.js installed correctly, npm will work too.
- Open Cursor by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor and press Enter to open it.
Using a different editor?
Any code editor works for this project. If you prefer VS Code or another editor, open that instead.
Create and initialize your project
You need a dedicated folder for your project files and a package.json file to manage your dependencies.
- Open the integrated terminal in Cursor by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir graphql-books
cd graphql-books
- Confirm the folder exists by running:
ls
The folder is empty, so you should see no output. That is expected.
- Open this folder in Cursor by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the graphql-books folder, then click Open.
- Initialize your project and enable ES modules by running this command in the terminal:
npm init --yes && npm pkg set type="module"
What does this command do?
The npm init --yes part creates a package.json file with default values. This file tracks your project's name, version, and dependencies.
The npm pkg set type="module" part enables ES module syntax (import/export) instead of the older require() style. Apollo Server uses ES module imports.
- Install Apollo Server and the GraphQL library by running:
npm install @apollo/server graphql
You should see output confirming that packages were added. A node_modules folder appears in your project.
Install failed or timed out?
Check that you are connected to the internet. If you see permission errors, try running the command again. If you see a version mismatch warning, ensure your Node.js version is 20 or higher by running node --version.
Help me troubleshoot my npm install error.
- Add a start script to your project by running:
npm pkg set scripts.start="node index.js"
What does the start script do?
This tells npm that when you run npm start, it should execute node index.js. This is the standard way to define how a Node.js project launches.
Create the entry file and verify
Your project needs an entry point file that Node.js will execute when you run npm start.
- In Cursor's Explorer sidebar (left panel), click the New File icon at the top of the file tree.
- Type index.js and press Enter to create the file.
- Leave the file empty for now. Save it with Cmd+S (macOS) or Ctrl+S (Windows).
- Test that your project runs by executing:
npm start
The command exits immediately with no output. This means Node.js successfully executed your empty index.js file without errors.
Seeing an error about missing index.js?
Make sure the file is named exactly index.js (not Index.js or index.JS). File names are case-sensitive. Also confirm the file is saved in the graphql-books folder, not a subfolder.
Help me fix my npm start error.
✔️ Awesome, I've got everything!
Great. Double check that package.json has "type": "module", a "start" script, and both @apollo/server and graphql in your dependencies.
ⓧ I'd like to double check the full code
Your package.json should contain these key fields. Your file may have additional fields from npm init (like description or main). That is fine.
{
"name": "graphql-books",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@apollo/server": "^4.11.3",
"graphql": "^16.14.2"
}
}
Your index.js file should exist and be empty.
Your project skeleton is ready with all the packages it needs. Next up, you will define your GraphQL schema with types for books and authors.
Define Your GraphQL Schema
Your project folder is set up with all the packages you need. Now it's time to define what data your API can serve and what operations clients can perform on it.
In GraphQL, everything starts with the schema. The schema is a blueprint that describes the shape of your data and the available operations. You'll write yours using SDL (Schema Definition Language), which reads like a contract between your server and any client that connects to it.
In this step, get ready to:
- Define Book and Author types in your schema.
- Add a Query type for fetching data.
- Add a Mutation type for creating data.
Import Apollo Server and define your data types
Before writing a schema, you need to import the modules that will eventually power your server. You also need to declare what kinds of objects your API knows about.
- Switch back to index.js in your editor and paste the following code:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Schema definition
const typeDefs = `#graphql
type Book {
id: ID!
title: String!
author: String!
year: Int
}
type Author {
name: String!
books: [Book!]!
}
`;
What does this code do?
- The two import lines bring in ApolloServer (to create your server later) and startStandaloneServer (to launch it). You won't use them until the next step, but importing them now keeps your file organised.
- The typeDefs variable holds your schema as a template literal string. The #graphql comment at the top enables syntax highlighting in editors with GraphQL extensions.
- The Book type defines four fields. The ! after a type name means that field can never be null.
- The Author type has a name and a list of associated books. The [Book!]! syntax means the list itself is non-nullable and every item inside it is also non-nullable.
- Save index.js.
- Run npm start in your terminal. It should exit immediately without any error messages. This confirms your JavaScript syntax is valid.
Seeing a syntax error?
- Check that your template literal uses backticks (`) and not regular quotes. The opening and closing backtick must both be present.
- Verify the import paths match exactly: '@apollo/server' and '@apollo/server/standalone'. Typos in the package path cause a MODULE_NOT_FOUND error.
- Make sure each type definition has matching curly braces.
Help me fix a syntax error in my GraphQL schema definition.
Add the Query type
Types describe data shapes, but clients also need a way to ask for that data. The Query type defines all the read operations your API supports.
- In index.js, find the closing of the Author type (the } after books: [Book!]!).
- Add the following Query type below it, still inside the template literal (before the closing backtick):
type Query {
books: [Book!]!
book(id: ID!): Book
}
What does the Query type do?
- books returns every book in the collection as a non-nullable list. Clients call this to fetch all books at once.
- book(id: ID!) accepts an id argument and returns a single Book. The return type is nullable (no !) because the ID might not match any book.
- Save index.js and run npm start. It should still exit without errors.
Getting an unexpected token error?
- Make sure the Query type sits after the closing } of the Author type, not inside it.
- Check that you have not accidentally placed code outside the backtick-delimited string.
Help me fix my Query type placement in the schema.
Add the Mutation type
Queries let clients read data. Mutations let clients change it. You need a Mutation type so clients can add new books to the collection.
- In index.js, find the closing } of the Query type you just added.
- Add the following Mutation type below it, still inside the template literal:
type Mutation {
addBook(title: String!, author: String!, year: Int): Book!
}
What does the Mutation type do?
- addBook accepts three arguments: title and author are required (String!), while year is optional (no !).
- The return type Book! means the mutation always returns the newly created book. This gives clients immediate confirmation of what was added.
- Save index.js.
- Run npm start one more time. It should exit cleanly without errors, confirming your complete schema is valid JavaScript.
Still seeing an error?
- Count your curly braces. Each type block needs an opening { and a closing }. Four types means four pairs.
- Confirm the template literal ends with a backtick followed by a semicolon: `;. A missing closing backtick causes the error to point at the end of the file.
Help me debug my full GraphQL schema.
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.js before moving on.
ⓧ I'd like to double check the full code
Here is the complete index.js at this point. Compare yours against it:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Schema definition
const typeDefs = `#graphql
type Book {
id: ID!
title: String!
author: String!
year: Int
}
type Author {
name: String!
books: [Book!]!
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String!, year: Int): Book!
}
`;
Your schema is complete. Next up, you'll bring it to life by writing resolvers that return real data and starting the server so you can test it.
Create Resolvers and Start the Server
Your schema defines what data clients can request. But it has no actual behavior yet. The schema is a blueprint waiting for instructions on how to fetch and modify data.
In this step, you'll create sample book data and write resolver functions that tell the server how to respond to each query and mutation. Then you'll boot up your Apollo Server and confirm it's ready to accept requests.
In this step, get ready to:
- Add sample book data to your server.
- Write resolver functions for queries, mutations, and authors.
- Start the Apollo Server and confirm it's running.
Add your sample book data
Resolvers need data to work with. You'll create an in-memory array of books that the server can query and modify.
- In index.js, find the closing of your typeDefs template literal (the line with just `;).
- Add the following sample data below it:
// Sample data
let books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 },
{ id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
{ id: '3', title: 'Go Set a Watchman', author: 'Harper Lee', year: 2015 },
{ id: '4', title: '1984', author: 'George Orwell', year: 1949 },
];
let nextId = 5;
What does this code do?
The books array holds your sample dataset in memory. Each book has an id, title, author, and year matching the fields in your Book type.
The nextId counter tracks which ID to assign to the next book a user creates. Since you already have books 1 through 4, it starts at 5.
- Save index.js.
Define your resolvers
A resolver is a function that tells GraphQL how to fetch or modify data for a specific field in your schema. You need one resolver for each field that isn't automatically handled by a parent object.
- Add the resolvers object below let nextId = 5; by pasting this code:
// Resolvers
const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => books.find((book) => book.id === id) || null,
},
Mutation: {
addBook: (_, { title, author, year }) => {
const newBook = { id: String(nextId++), title, author, year: year || null };
books.push(newBook);
return newBook;
},
},
Author: {
books: (parent) => books.filter((book) => book.author === parent.name),
},
};
What does this code do?
The resolvers object maps directly to the types in your schema. Each function handles one field:
- Query.books returns the entire books array.
- Query.book uses the id argument to find a single book. The underscore _ is a convention for unused parameters.
- Mutation.addBook creates a new book object, assigns it a unique ID with nextId++, pushes it into the array, and returns it.
- Author.books filters the books array to find all books by a given author. The parent parameter holds the Author object being resolved.
- Save index.js.
Seeing a syntax error?
The resolvers object has nested braces and commas that are easy to miss. Check that every opening { has a matching } and that each resolver function ends with a comma except the last one in its group.
help me find the syntax error in my resolvers object
Create and start the Apollo Server
With your schema and resolvers defined, you can now create the server instance and start it listening for requests.
- Add the server setup code at the bottom of index.js, below your resolvers object:
// Server setup
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`Server ready at: ${url}`);
What does this code do?
ApolloServer takes your typeDefs and resolvers to create a server that knows both the shape of your data and how to fetch it.
startStandaloneServer spins up an HTTP server on port 4000 and returns the URL where it's listening. The top-level await works because your project uses ES modules.
- Save index.js.
- Start your server by running this command in your terminal:
npm start
- You should see this message in your terminal:
Server ready at: http://localhost:4000/
Don't see the server ready message?
- If you see EADDRINUSE, another process is using port 4000. Close any other servers running on that port and try again.
- If you see a SyntaxError, check that your resolvers object and server setup code both have matching braces and correct comma placement.
- Make sure you saved index.js before running npm start.
help me debug why my Apollo Server won't start
✔️ Awesome, I've got everything!
Great. Make sure you've saved index.js and keep the server running for the next step.
ⓧ I'd like to double check the full code
Here is the full index.js file up to this point:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Schema definition
const typeDefs = `#graphql
type Book {
id: ID!
title: String!
author: String!
year: Int
}
type Author {
name: String!
books: [Book!]!
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String!, year: Int): Book!
}
`;
// Sample data
let books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 },
{ id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
{ id: '3', title: 'Go Set a Watchman', author: 'Harper Lee', year: 2015 },
{ id: '4', title: '1984', author: 'George Orwell', year: 1949 },
];
let nextId = 5;
// Resolvers
const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => books.find((book) => book.id === id) || null,
},
Mutation: {
addBook: (_, { title, author, year }) => {
const newBook = { id: String(nextId++), title, author, year: year || null };
books.push(newBook);
return newBook;
},
},
Author: {
books: (parent) => books.filter((book) => book.author === parent.name),
},
};
// Server setup
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`Server ready at: ${url}`);
Your GraphQL API is alive and listening for requests. Next up, you'll open Apollo Sandbox and start querying your data interactively.
Query Your API in Apollo Sandbox
Your server is running, your schema is defined, and your resolvers are wired up. Now it is time to actually talk to your API.
Apollo Sandbox is a browser-based GraphQL explorer that loads automatically when you visit your server URL. You will use it to write queries and mutations, run them, and inspect the responses in real time.
In this step, get ready to:
- Query all books in your collection.
- Look up a single book by its ID.
- Add a new book with a mutation.
Open Apollo Sandbox and query all books
Apollo Sandbox gives you an interactive editor where you can write GraphQL operations and immediately see the results. No extra installation needed.
- Open your browser and navigate to http://localhost:4000/.
- Apollo Sandbox loads automatically. You will see the Operation panel in the center where you write queries.
- Clear any default content in the Operation panel, then paste the following query:
query {
books {
title
author
}
}
- Click the blue run button at the top of the Operation panel.
What does this query do?
The query keyword tells GraphQL you want to read data. Inside the curly braces, books is the field you defined in your Query type.
The nested title and author inside books are the specific fields you want returned. This is the power of GraphQL. You ask for exactly what you need, nothing more.
In the Response panel on the right, you should see a JSON object containing all four books from your sample data, each with only their title and author fields.
Seeing a connection error?
- Make sure your server is still running in the terminal. You should see Server ready at: http://localhost:4000/ in the output. If the process stopped, run npm start again.
- If the browser shows a blank page or a "cannot connect" message, confirm you are visiting http://localhost:4000/ (not https).
Help me troubleshoot why Apollo Sandbox is not loading at localhost:4000.
Fetch a single book by ID
Your schema also exposes a book query that accepts an id argument. This lets clients request one specific book instead of the entire collection.
- Clear the Operation panel and paste this query:
query {
book(id: "1") {
title
author
year
}
}
- Click the run button.
What does the id argument do?
The book(id: "1") syntax passes an argument to the book resolver. Your resolver uses this id to find a matching book in the array.
You requested three fields this time: title, author, and year. GraphQL returns exactly those three fields and nothing else.
The Response panel should show "The Great Gatsby" by F. Scott Fitzgerald, published in 1925.
- Now test what happens with an ID that does not exist. Replace "1" with "999" in the query so it looks like this:
query {
book(id: "999") {
title
author
year
}
}
- Click the run button.
The Response panel shows null for the book field. This is expected because your resolver returns null when no book matches the given ID.
Why null instead of an error?
In your schema, the book query returns Book (nullable, no !). That means "this field might not return a value." GraphQL uses this to communicate that a missing result is a normal outcome, not a server error.
Add a new book with a mutation
Queries read data. Mutations change data. Your addBook mutation lets clients add a new book to the collection and get back the created book in the same request.
- Clear the Operation panel and paste this mutation:
mutation {
addBook(title: "Brave New World", author: "Aldous Huxley", year: 1932) {
id
title
}
}
- Click the run button.
What does this mutation do?
The mutation keyword tells GraphQL you want to write data. addBook receives three arguments (title, author, year) and creates a new book.
The id and title inside the curly braces are the fields you want returned from the newly created book. Your resolver auto-generates the id, so you can confirm the book was created successfully.
The Response panel should show an object with id of "5" and title of "Brave New World".
- Now confirm the new book persists. Clear the Operation panel and paste your original all-books query:
query {
books {
title
author
}
}
- Click the run button.
You should now see five books in the Response panel. "Brave New World" by Aldous Huxley appears at the end of the list.
Why does the new book persist?
Your mutation pushes the new book into the books array in memory. As long as the server keeps running, that array retains its changes. If you restart the server (Ctrl+C then npm start), the data resets to the original four books.
Mutation not returning the expected data?
- Check that the Operation panel starts with mutation (not query). Using the wrong keyword causes a field-not-found error.
- Make sure string arguments are wrapped in double quotes. title: "Brave New World" is correct. title: Brave New World without quotes causes a syntax error.
- Confirm your server is still running in the terminal.
Help me fix my addBook mutation in Apollo Sandbox.
Secret mission
Add Delete and Filtering
Your API can add books, but what about removing them? And what if a client only wants books by a specific author? Extend your schema with a deleteBook mutation and an author filter on your books query.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files, stop the running server, or delete everything entirely.
Resources you used:
- The graphql-books project folder on your Desktop (includes index.js, package.json, and node_modules/).
- A running Node.js process serving Apollo Server on port 4000.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with your GraphQL API.
- Your server continues running at http://localhost:4000/ until you close the terminal or press Ctrl+C.
- Your project files stay on your Desktop in the graphql-books folder.
- Node.js and npm remain installed on your machine.
Pause - I'll come back to this later
Stop the running server to free up port 4000, but keep your project files for next time.
- In the terminal where your server is running, press Ctrl+C to stop the process.
- Your graphql-books folder stays on your Desktop. To restart later, open a terminal in that folder and run npm start.
Delete - I don't want to use this again
Remove all project files and stop the server completely.
- In the terminal where your server is running, press Ctrl+C to stop the process.
- Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/graphql-books
On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\graphql-books
- Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir %USERPROFILE%\Desktop (Windows). You should no longer see graphql-books in the output.
Nice Work!
Nice Work!
You just built a fully functional GraphQL API from scratch using Apollo Server and Node.js. You went from an empty folder to a running server that handles queries, mutations, and type relationships.
Here's what you learned:
- Defined a GraphQL schema with custom types, queries, and mutations using SDL (Schema Definition Language).
- Wrote resolvers that fetch, filter, and mutate in-memory data to power your API operations.
- Queried and mutated data interactively using Apollo Sandbox to test your API in real time.
- Secret Mission: Added a deleteBook mutation and author filtering to make the API production-realistic.
Ready to quiz yourself?