Build a Bookstore GraphQL API

Build a GraphQL API with Apollo Server using schemas, resolvers, and mutations.

Introduction

30 Second Summary

When you build a REST API, every endpoint returns the same fixed shape of data. Need just a book's title? You still get the entire book object back, plus every other field. Need a book and its author? That's two separate requests to two different endpoints.

In this project, you will build a GraphQL API for a bookstore using Apollo Server. You'll define a schema, write resolvers, and experience how GraphQL lets clients ask for exactly the data they need in a single request.

What You'll Build

You'll type a query like "give me this book's title and its author's name" into a browser playground, hit run, and get back precisely that data in one response.

By the end of this project, you'll have:

  • A GraphQL schema that defines your bookstore's data types, queries, and mutations so clients can request books, authors, and relationships in a single query.
  • Field resolvers that automatically connect books to authors, letting clients traverse nested data without extra requests.
  • Mutations for creating and deleting books, completing a full CRUD cycle through GraphQL's query language.
  • Secret Mission: Add a search filter that lets clients find books by title keyword.

Are there any prerequisites?

This project assumes you have basic REST API knowledge (completed "Build a Bookstore REST API with Express" or equivalent). You also need Node.js v20 or higher installed on your machine.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters.

You already know how to build a REST API with Express. This project takes that foundation and rebuilds the same bookstore data layer using GraphQL, where clients ask for exactly the data they need in a single request.

Set Up Your GraphQL Project

Every GraphQL server starts with a project folder, a schema, and a runtime that can serve queries. Before you can define any schemas or write resolvers, you need a Node.js project wired up with Apollo Server.

In this step, you will create a fresh project, install the packages that power your GraphQL server, and confirm everything runs.

In this step, get ready to:
  • Create a project folder and initialize npm.
  • Install Apollo Server and its dependencies.
  • Create the entry file and confirm Node.js runs it.
Create a new project folder and initialize it

Your GraphQL project needs its own folder with a package.json file that tracks dependencies and project settings.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
  • Confirm your Node.js version by running this command:
node --version

You should see v20 or higher in the output.

Seeing a version lower than v20?

This project requires Node.js v20 or later. Visit the official Node.js downloads page at nodejs.org to install the latest version.

After installing, close and reopen your terminal, then run node --version again to confirm the update. Why is my Node.js version still old after installing a new one?

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir bookstore-graphql
cd bookstore-graphql
  • Initialize a new Node.js project by running:
npm init -y

What does npm init -y do?

This command generates a package.json file with sensible defaults. The -y flag skips all the interactive prompts and accepts every default value automatically.

  • Confirm the file was created by running:
ls

You should see package.json listed in the output.

  • Open this folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Select the bookstore-graphql folder on your Desktop, then click Open.

VS Code now shows your project folder in the Explorer sidebar on the left.

  • Open the integrated terminal again by pressing Ctrl+` (Windows) or Cmd+` (macOS).
  • Open package.json in the editor by clicking it in the Explorer sidebar.
  • Replace the entire contents of package.json with this code:
{
  "name": "bookstore-graphql",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  }
}

What does type: module do?

Adding "type": "module" tells Node.js to treat your .js files as ES modules. This enables the modern import/export syntax instead of the older require() style.

  • Save package.json by pressing Ctrl+S (Windows) or Cmd+S (macOS).
Install Apollo Server and GraphQL

Apollo Server is the library that turns your type definitions and resolvers into a running GraphQL server. It needs the core graphql package as a companion to parse and validate queries.

  • Install both packages by running this command in your terminal:
npm install @apollo/server graphql

You will see npm download the packages and add them to a node_modules folder.

What did npm install?

  • @apollo/server is the GraphQL server library that handles incoming queries, validates them against your schema, and runs your resolvers.
  • graphql is the reference implementation of the GraphQL specification. Apollo Server uses it under the hood to parse query strings and validate types.
  • Verify the packages appear in your package.json by switching back to the editor tab.

You should now see a dependencies section listing @apollo/server and graphql with version numbers.

Not seeing the dependencies?

Make sure you ran the npm install command from inside the bookstore-graphql folder. If your terminal shows a different path, run cd ~/Desktop/bookstore-graphql and try the install again.

Why is npm install not adding dependencies to my package.json?

Create the entry file and verify Node.js is ready

Your server needs an entry point file that Node.js will execute. For now, a simple log statement proves everything is wired up correctly.

  • Create a new file called index.js by clicking the new file icon in the Explorer sidebar (or pressing Ctrl+N then Ctrl+S and naming it index.js).
  • Add this code to index.js:
console.log('GraphQL server starting...');

What does this do?

This single line prints a message to the terminal when you run the file. It confirms that Node.js can find and execute your index.js file without errors.

  • Save index.js.

Before you run this, what do you expect to see in the terminal?

  • Run your file by executing this command in the terminal:
node index.js

You should see GraphQL server starting... printed in the terminal. That confirms Node.js is executing your project entry file successfully.

Seeing an error instead of the message?

If you see a "file not found" error, make sure your terminal is in the bookstore-graphql folder and that the file is saved as index.js (not index.js.txt or another name).

If you see a syntax error, check that your console.log line matches the code above exactly, including the quotes and parentheses.

Why is Node.js unable to find or run my index.js file?

✔️ Awesome, I've got everything!

Great. Double check you have saved both package.json and index.js.

ⓧ I'd like to double check the full code

Here is what each file should look like at this point.

package.json:

{
  "name": "bookstore-graphql",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "@apollo/server": "^5.5.1",
    "graphql": "^16.14.2"
  }
}

index.js:

console.log('GraphQL server starting...');

Your project foundation is locked in. Next up, you will define your first GraphQL schema and run your very first query in Apollo Sandbox.

Define Your Schema and Run Your First Query

Your project folder is set up and Apollo Server is installed. Now you need to tell your server what data it can serve and how to fetch it.

A GraphQL server needs two things: a schema that describes the shape of your data, and resolvers that tell it where to get the data. In this step, you will define both and run your first query.

In this step, get ready to:
  • Define a Book type and a query in your GraphQL schema.
  • Create and start an Apollo Server instance.
  • Test your first query in Apollo Sandbox.
Define the Book type and a books query

Every GraphQL API starts with a schema. The schema declares what types of data exist and what queries clients can run against them.

  • In VS Code, open index.js in your bookstore-graphql folder.
  • Select all the existing code (the console.log line) and replace it with the following imports and schema definition:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
  type Book {
    id: ID!
    title: String!
    year: Int
  }

  type Query {
    books: [Book!]!
  }
`;

What does this code do?

The two import lines bring in Apollo Server and its standalone HTTP helper. You will use these to create and start the server later in this step.

The typeDefs string is your GraphQL schema. It defines a Book type with three fields (id, title, year) and a Query type that tells clients they can ask for a list of books.

  • Save index.js and confirm the imports resolve by running this command in your terminal:
node index.js

The script should exit silently with no errors. If you see an error about modules not being found, check that your package.json contains "type": "module" and that you ran npm install in the previous step.

Seeing a module error?

If you see Cannot use import statement outside a module, open package.json and confirm it has "type": "module" at the top level. Save and re-run.

If you see Cannot find package, make sure you are in the bookstore-graphql folder and run npm install again.

Still stuck? Help me fix this module import error in my Apollo Server project.

Now add the sample book data and the resolver that serves it. Paste this code below the typeDefs block in index.js:

let books = [
  { id: '1', title: 'The Great Gatsby', year: 1925, authorId: '1' },
  { id: '2', title: 'To Kill a Mockingbird', year: 1960, authorId: '2' },
  { id: '3', title: 'Go Set a Watchman', year: 2015, authorId: '2' },
  { id: '4', title: '1984', year: 1949, authorId: '3' },
  { id: '5', title: 'Animal Farm', year: 1945, authorId: '3' },
];

const resolvers = {
  Query: {
    books: () => books,
  },
};

What does this code do?

The books array is your in-memory data store. Each book has an id, title, year, and an authorId you will use in a later step.

The resolvers object maps schema fields to functions. When a client queries books, GraphQL calls Query.books which returns the entire array.

  • Save index.js.
Create the Apollo Server and start it

You have a schema and a resolver, but no server to serve them. Now you will wire them together and start listening on port 4000.

  • Add the following code at the bottom of index.js, below the resolvers object:
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?

The ApolloServer constructor takes your schema (typeDefs) and resolver functions (resolvers) and bundles them into a server instance.

The startStandaloneServer helper spins up an HTTP server on port 4000 and returns the URL. The console.log confirms everything is running.

  • Save index.js.
  • Before you run the server, predict what you expect to see in the terminal. Will it print anything?
  • Start the server by running:
node index.js

You should see Server ready at: http://localhost:4000/ printed in the terminal.

Server not starting?

If you see a syntax error, make sure you have "type": "module" in package.json. Top-level await only works in ES modules.

If you see a port conflict, another process is already using port 4000. Close it or change the port number in your code.

Still stuck? Help me debug why my Apollo Server won't start on port 4000.

✔️ Awesome, I've got everything!

Great. Double check you have saved index.js and your server is printing the ready message.

ⓧ I'd like to double check the full code

Scroll up through your file and confirm these sections appear in order:

  • Two import lines at the top (ApolloServer and startStandaloneServer).
  • The typeDefs template literal with Book and Query types.
  • The books array with 5 entries.
  • The resolvers object with Query.books.
  • The ApolloServer constructor, startStandaloneServer call, and console.log.
Test in Apollo Sandbox

Your server is running. Apollo Sandbox is a browser-based GraphQL playground that loads automatically when you visit your server's URL.

  • Open your browser and navigate to http://localhost:4000.
  • Apollo Sandbox loads with the Explorer interface. You will see three panels: Documentation on the left, the Operation panel in the middle, and the Response panel on the right.
  • In the Operation panel, clear any existing text and type the following query:
query {
  books {
    id
    title
    year
  }
}
  • Click the blue Run button at the top of the Operation panel.

The Response panel on the right shows all five books as JSON, each with their id, title, and year.

Why is this different from REST?

A REST endpoint always returns a fixed shape. If /books returns 10 fields, you get all 10 whether you need them or not. GraphQL lets the client decide exactly which fields to request.

  • Now try requesting fewer fields. Replace your query in the Operation panel with:
query {
  books {
    title
  }
}
  • Click Run again.

This time the response contains only the title field for each book. No id, no year. You asked for exactly what you needed and that is exactly what you got back.

Sandbox not loading?

Make sure your terminal still shows the server running. If you closed it or pressed Ctrl+C, re-run node index.js first.

If the page loads but shows a connection error, check that the URL in the Sandbox connection bar matches http://localhost:4000.

Still stuck? Help me connect Apollo Sandbox to my local server.

Your GraphQL server is live and answering queries. Next up, you will add authors and discover what happens when you try to fetch related data across types.

Add Authors and Hit the Nesting Wall

Your books query works, but look at the data. Every book has an authorId field pointing to an author, yet there is no way to get author details.

In a REST API, you would need a second request to /authors/:id for each book. In GraphQL, you want to write a single query like book { author { name } } and get everything back at once. Let's see what happens when we try.

In this step, get ready to:
  • Add an Author type and sample author data.
  • Connect books and authors in the schema with relationship fields.
  • Hit the nesting wall and observe the problem that field resolvers solve.
Add the Author type and authors data

Your schema currently knows about books but has no concept of authors. You need an Author type and query fields so clients can fetch author data.

  • In VS Code, open index.js and find the typeDefs string.
  • Replace the entire typeDefs variable with this updated version that adds an Author type and expands the Query type:
const typeDefs = `#graphql
  type Book {
    id: ID!
    title: String!
    year: Int
  }

  type Author {
    id: ID!
    name: String!
  }

  type Query {
    books: [Book!]!
    book(id: ID!): Book
    authors: [Author!]!
    author(id: ID!): Author
  }
`;

What does this code do?

You added two new things to your schema:

  • An Author type with id and name fields. This tells GraphQL what shape author data has.
  • New query fields: book(id: ID!) fetches a single book, authors fetches all authors, and author(id: ID!) fetches a single author. The ! after ID means the argument is required.
  • Now add the authors data array and update your resolvers. Find the const resolvers line and replace everything from there down to the closing }; of the resolvers object with this code:
// Sample author data matching each book's authorId
const authors = [
  { id: '1', name: 'F. Scott Fitzgerald' },
  { id: '2', name: 'Harper Lee' },
  { id: '3', name: 'George Orwell' },
];

const resolvers = {
  Query: {
    books: () => books,
    book: (_, { id }) => books.find((book) => book.id === id),
    authors: () => authors,
    author: (_, { id }) => authors.find((author) => author.id === id),
  },
};

What does this code do?

The authors array has three authors whose id values match the authorId in your books data.

The resolvers use find() to look up items by ID. The underscore _ is a convention for an unused first argument (the parent object, which is empty for top-level queries). The second argument is the query arguments the client passes in.

  • Save index.js.
  • Stop the running server in your terminal by pressing Ctrl+C.
  • Restart the server by running:
node index.js
  • Switch back to Apollo Sandbox in your browser. Paste this query into the Operations panel and click the blue run button:
query {
  authors {
    id
    name
  }
}

You should see all three authors returned in the Response panel with their IDs and names.

Not seeing your authors?

  • Make sure you stopped and restarted the server. Apollo Sandbox introspects the schema on connect, so it needs a fresh server to pick up your changes.
  • Check that authors is defined after the books array and before resolvers in your file.
  • Confirm your resolver spells authors exactly as it appears in the Query type of your typeDefs.

help me debug why my authors query is not returning data

Add the relationship fields to the schema

Your authors query works, but books and authors are still disconnected. You want to query a book and get its author in the same response. That means your schema needs relationship fields that connect the two types.

  • In index.js, find the Book type inside your typeDefs. It currently looks like this:
  type Book {
    id: ID!
    title: String!
    year: Int
  }
  • Add author: Author as a new field below year: Int, so it now looks like this:
  type Book {
    id: ID!
    title: String!
    year: Int
    author: Author
  }
  • Now find the Author type. It currently looks like this:
  type Author {
    id: ID!
    name: String!
  }
  • Add books: [Book!]! as a new field below name: String!, so it now looks like this:
  type Author {
    id: ID!
    name: String!
    books: [Book!]!
  }

What do these relationship fields mean?

The author: Author field on Book says: "every book can have an associated author." The books: [Book!]! field on Author says: "every author has a list of books." These fields create a two-way relationship in your schema.

The [Book!]! syntax means the field always returns an array (the outer !), and every item in that array is a non-null Book (the inner !).

  • Save index.js.
Query the nested data and observe the failure

Your schema now declares that books have authors. Let's see if GraphQL can actually resolve that relationship.

  • Stop the server with Ctrl+C and restart it by running:
node index.js

Before you run the next query, think about what you expect to happen. You declared author: Author on the Book type. Do you think GraphQL will automatically connect books to their authors?

  • In Apollo Sandbox, paste this query into the Operations panel and click the blue run button:
query {
  books {
    title
    author {
      name
    }
  }
}

The author field comes back as null for every single book.

Why is author null?

GraphQL sees the author field on the Book type and looks for a property called author on each book object. But your book data only has authorId (a string like "1"), not an author object.

There is no resolver telling GraphQL how to turn authorId into an actual Author. That missing piece is called a field resolver, and it is the fix you will build in the next step.

✔️ Awesome, I've got everything!

Great. Double check you have saved index.js and confirmed the null result in Sandbox.

ⓧ I'd like to double check the full code

Your index.js should now look like this:

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
  type Book {
    id: ID!
    title: String!
    year: Int
    author: Author
  }

  type Author {
    id: ID!
    name: String!
    books: [Book!]!
  }

  type Query {
    books: [Book!]!
    book(id: ID!): Book
    authors: [Author!]!
    author(id: ID!): Author
  }
`;
let books = [
  { id: '1', title: 'The Great Gatsby', year: 1925, authorId: '1' },
  { id: '2', title: 'To Kill a Mockingbird', year: 1960, authorId: '2' },
  { id: '3', title: 'Go Set a Watchman', year: 2015, authorId: '2' },
  { id: '4', title: '1984', year: 1949, authorId: '3' },
  { id: '5', title: 'Animal Farm', year: 1945, authorId: '3' },
];

// Sample author data matching each book's authorId
const authors = [
  { id: '1', name: 'F. Scott Fitzgerald' },
  { id: '2', name: 'Harper Lee' },
  { id: '3', name: 'George Orwell' },
];

const resolvers = {
  Query: {
    books: () => books,
    book: (_, { id }) => books.find((book) => book.id === id),
    authors: () => authors,
    author: (_, { id }) => authors.find((author) => author.id === id),
  },
};
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`Server ready at: ${url}`);

You have exposed the gap that makes nested GraphQL queries powerful. Your schema declares the relationship, but without a field resolver to bridge authorId to an Author object, GraphQL returns null. Next up, you will write the field resolvers that close this gap.

Connect Books to Authors with Field Resolvers

In the last step, you saw the problem firsthand: every book's author field returned null. Your schema says a Book has an Author, but your data only stores an authorId string. GraphQL has no idea how to bridge that gap on its own.

Field resolvers are what make GraphQL powerful. They tell GraphQL exactly how to fetch nested data when a client requests it. In this step, you'll write field resolvers that transform a raw authorId into a real Author object.

In this step, get ready to:
  • Add a field resolver that connects each book to its author.
  • Add a field resolver that connects each author to their books.
  • Run flexible queries that traverse relationships in both directions.
Add a Book.author field resolver

A field resolver runs when a client requests a specific field. When someone queries author on a Book, GraphQL calls the Book.author resolver and passes the current book as the parent argument.

  • In index.js, find the closing }, of the Query block inside your resolvers object.
  • Add the following Book resolver block directly after it (still inside the resolvers object):
  Book: {
    // Look up the author whose ID matches this book's authorId
    author: (parent) => authors.find((author) => author.id === parent.authorId),
  },

What does this code do?

The parent parameter is the book object that GraphQL is currently resolving. It contains id, title, year, and authorId.

The resolver uses parent.authorId to search the authors array and return the matching author object. GraphQL then uses that object to fill in the name field (or any other Author field the client requested).

  • Save index.js.
  • Switch to your terminal and stop the running server by pressing Ctrl+C.
  • Restart the server by running:
node index.js

Before you test, think back to the query that returned null for every author. What do you expect to see this time when you run it?

  • Switch to Apollo Sandbox in your browser and run this query:
query {
  books {
    title
    author {
      name
    }
  }
}

Every book now returns its author's name instead of null. The field resolver you just wrote is doing the lookup behind the scenes.

Still seeing null for author?

  • Make sure the Book block is inside the resolvers object (after Query's closing brace), not outside it.
  • Check that the function uses parent.authorId (capital I), matching the property name in your books array.
  • Confirm you restarted the server after saving. Code changes only take effect after a restart.

Why is my Book.author field resolver still returning null?

Add an Author.books field resolver

The relationship works in one direction now. You can go from a book to its author. To complete the connection, you need a resolver that goes the other way: from an author to all their books.

  • In index.js, add the following Author resolver block directly after the Book block you just added:
  Author: {
    // Find all books whose authorId matches this author's id
    books: (parent) => books.filter((book) => book.authorId === parent.id),
  },

What does this code do?

This time, parent is the author object currently being resolved. The resolver uses filter (not find) because one author can have multiple books.

  • Save index.js.
  • Stop the server with Ctrl+C and restart it by running node index.js.
  • In Apollo Sandbox, run this query to test the reverse direction:
query {
  authors {
    name
    books {
      title
      year
    }
  }
}

Each author now lists their books. Harper Lee shows two titles, George Orwell shows two, and F. Scott Fitzgerald shows one.

Seeing an empty books array for every author?

  • Check that your filter compares book.authorId === parent.id. Swapping the comparison (using book.id instead of book.authorId) won't find any matches.
  • Make sure the Author block is inside the resolvers object, not outside it.

Why is my Author.books resolver returning an empty array?

✔️ Awesome, I've got everything!

Great. Make sure you've saved your file before moving on.

ⓧ I'd like to double check the full code

Your index.js should contain these sections in order:

  • Imports for ApolloServer and startStandaloneServer.
  • The typeDefs string with Book, Author, and Query types.
  • The books array (5 entries) and authors array (3 entries).
  • The resolvers object with three blocks: Query, Book (with author function), and Author (with books function).
  • The ApolloServer instantiation and startStandaloneServer call on port 4000.

Scroll up through this step and confirm each code addition is in place.

Experience the power of flexible queries

With both field resolvers in place, you can now write a single query that fetches a book by ID, grabs its title and year, and follows the relationship to get the author's name. In a REST API, this would require at least two separate requests.

  • In Apollo Sandbox, run this query:
query {
  book(id: "1") {
    title
    year
    author {
      name
    }
  }
}

You get back exactly the fields you asked for: the title, year, and author name for The Great Gatsby. One request, zero wasted data.

How does this compare to REST?

With your REST API from the previous project, you would need to call /books/1 to get the book, then use the authorId from the response to make a second request to /authors/:id. That is two round-trips and both responses include fields you might not need.

With GraphQL, you get exactly what you ask for in a single request. The field resolver handles the join behind the scenes.

  • Try requesting just books { title } (without the author field). Notice GraphQL only returns titles. The author resolver never runs because you did not ask for it.

Why does this matter?

Field resolvers only execute when the client requests that field. If nobody asks for author, the lookup never happens. This is how GraphQL avoids over-fetching by default.

Your API can now traverse relationships in both directions with a single query. Next up, you'll add mutations so clients can create and delete books through the same GraphQL interface.

Add Mutations for Creating and Deleting Books

Your GraphQL API can now serve deeply nested data in a single query. Queries let clients read data, but a real API also needs write operations for creating and deleting records.

GraphQL mutations are the equivalent of POST and DELETE in REST. They use the same query language you already know, but they modify data instead of just reading it. In this step, you'll add mutations that let clients create new books and remove existing ones.

In this step, get ready to:
  • Define a Mutation type in your schema with addBook and deleteBook operations.
  • Write resolver functions that create and delete books from your data.
  • Test the full create-and-delete cycle in Apollo Sandbox.
Define the Mutation type in your schema

Just like the Query type tells GraphQL what clients can read, a Mutation type tells GraphQL what clients can write.

  • In index.js, find the closing brace of your Query type inside typeDefs. It looks like this:
  type Query {
    books: [Book!]!
    book(id: ID!): Book
    authors: [Author!]!
    author(id: ID!): Author
  }
  • Add the following Mutation type directly below the Query type's closing brace:
  type Mutation {
    addBook(title: String!, authorId: ID!, year: Int): Book!
    deleteBook(id: ID!): Boolean!
  }

What does this code do?

  • addBook accepts a required title, a required authorId, and an optional year. It guarantees a Book is returned (the ! means non-null).
  • deleteBook accepts a required id and returns a Boolean! indicating success or failure.
  • The ! on argument types means the client must provide that value. The ! on return types means the server guarantees a non-null response.
  • Save index.js.
Write the mutation resolvers

Your schema now declares what mutations exist, but GraphQL needs resolver functions to know what to actually do when a client calls them.

  • In index.js, find the authors array. Add this line directly below it (before the const resolvers line):
let nextBookId = 6;
  • Now find the Author resolver inside your resolvers object. It looks like this:
  Author: {
    books: (parent) => books.filter((book) => book.authorId === parent.id),
  },
  • Add the following Mutation resolver directly after the Author resolver's closing brace and comma:
  Mutation: {
    addBook: (_, { title, authorId, year }) => {
      const newBook = { id: String(nextBookId++), title, authorId, year };
      books.push(newBook);
      return newBook;
    },
    deleteBook: (_, { id }) => {
      const index = books.findIndex((book) => book.id === id);
      if (index === -1) return false;
      books.splice(index, 1);
      return true;
    },
  },

What does this code do?

  • nextBookId tracks the next available ID. Each time addBook runs, it increments automatically with nextBookId++.
  • addBook creates a new book object, pushes it into the books array, and returns the new book. Because you already have a Book.author field resolver, the returned book automatically resolves its author.
  • deleteBook finds the book by ID using findIndex, removes it with splice, and returns true. If no book matches, it returns false.
  • The first argument _ is the parent (unused at the top level). The second argument destructures the arguments the client passed in the mutation.
  • Save index.js.
  • Stop your running server with Ctrl+C in the terminal, then restart it by running:
node index.js

You should see Server ready at: http://localhost:4000/ printed in the terminal.

Server not starting?

  • Check for a missing comma after the Author resolver's closing brace. The Mutation key must be separated from Author by a comma.
  • Make sure let nextBookId = 6; is placed outside the resolvers object, not inside it.
  • Verify the Mutation type in your typeDefs is inside the backtick string, not outside it.

Still stuck? Help me debug my Apollo Server startup error.

Test the full CRUD cycle

Your server now supports both read and write operations. Time to create a book, see it appear, then delete one and confirm it disappears.

  • Switch back to Apollo Sandbox in your browser at http://localhost:4000.
  • Clear any previous query in the editor and paste this addBook mutation:
mutation {
  addBook(title: "Brave New World", authorId: "3", year: 1932) {
    id
    title
    author {
      name
    }
  }
}
  • Click the run button to execute the mutation.

You should see a response with the new book's id, title, and its author resolved to George Orwell. All in one response.

Why does the author resolve automatically?

Your addBook resolver returns a book object with an authorId field. When the client requests author { name }, GraphQL calls your Book.author field resolver to look up the matching author. The field resolvers you wrote in the previous step keep working for newly created books too.

Now test deletion. Before you run the next mutation, what do you think will happen if you delete a book that exists?

  • Paste this deleteBook mutation into the editor:
mutation {
  deleteBook(id: "1")
}
  • Click the run button to execute.

You should see true in the response, confirming the book was deleted.

  • Verify the deletion by running a books query:
{
  books {
    title
  }
}

The Great Gatsby is gone from the list, and Brave New World (the book you just created) appears. Your mutations are working.

✔️ Awesome, I've got everything!

Great. Double check you've saved index.js and your server is running.

ⓧ I'd like to double check the full code

Your index.js now has these sections in order. Scroll up and confirm each is present:

  • Imports for ApolloServer and startStandaloneServer.
  • typeDefs string with Book, Author, Query, and Mutation types.
  • books array (5 entries) declared with let.
  • authors array (3 entries) declared with const.
  • let nextBookId = 6; between the arrays and the resolvers.
  • resolvers object with Query, Book, Author, and Mutation keys.
  • Server creation and startStandaloneServer call on port 4000.

Secret mission

Add Search Filtering to Your Queries

Your books query returns everything every time. Real APIs let clients filter results. Can you add a search argument that finds books by title keyword without breaking existing queries?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The bookstore-graphql project folder (contains index.js, package.json, and node_modules/).

Keep everything running

No action needed. Choose this if you're still actively building or want to experiment with more queries and mutations.

  • Your bookstore-graphql folder stays on your Desktop for future reference.
  • The server only runs when you start it with node index.js. It does not consume resources when idle.
  • Node.js remains installed on your machine (you likely use it for other projects too).

Pause - I'll come back to this later

Shut down the running server to free up the terminal. Your project files stay on disk for when you return.

  • If your server is still running, press Ctrl+C in the terminal to stop it.
  • Your bookstore-graphql folder remains on your Desktop. When you want to resume, open the folder in VS Code, open a terminal, and run node index.js.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes everything you built in this project.

  • If your server is still running, press Ctrl+C in the terminal to stop it.
  • Delete the project folder by running the command below for your operating system.

macOS

cd ~/Desktop
rm -rf bookstore-graphql

Windows

cd ~/Desktop
Remove-Item -Recurse -Force bookstore-graphql
  • Run ls (macOS) or dir (Windows) to confirm bookstore-graphql no longer appears on your Desktop.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional GraphQL API from scratch using Apollo Server.

You've learned how to:

  • Defined a GraphQL schema with types, queries, and mutations using Schema Definition Language, and connected it to data through resolvers.
  • Written field-level resolvers that connect books to authors and authors to books, letting clients fetch nested relationships in a single request.
  • Added mutations for creating and deleting books, completing a full CRUD cycle through GraphQL's query language.
  • Secret Mission: Implemented search filtering with optional query arguments that return matching books by title keyword.

Ready to quiz yourself?