This tutorial sets up a GraphQL server with Express, Node.js, and express-graphql to manage a list of books with CRUD operations, addressing the issue where the book(id: “1”) query returns null.
Create a new directory and initialize a Node.js project:
mkdir graphql-express-tutorial
cd graphql-express-tutorial
npm init -y
Install required dependencies:
npm install express express-graphql graphql
Define the GraphQL schema with types, queries, and mutations in schema.js:
const { buildSchema } = require('graphql');
const schema = buildSchema(` type Book { id: ID! title: String! author: String! }
type Query { books: [Book!]! book(id: ID!): Book }
type Mutation { addBook(title: String!, author: String!): Book! updateBook(id: ID!, title: String, author: String): Book! deleteBook(id: ID!): Boolean! } `);
module.exports = schema;
Implement resolvers with debugging logs in resolvers.js:
let books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' },
{ id: '2', title: '1984', author: 'George Orwell' },
];
const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => {
console.log('book resolver called with id:', id, 'type:', typeof id);
return books.find(book => book.id === id);
},
},
Mutation: {
addBook: (_, { title, author }) => {
const book = { id: String(books.length + 1), title, author };
books.push(book);
return book;
},
updateBook: (_, { id, title, author }) => {
const bookIndex = books.findIndex(book => book.id === id);
if (bookIndex === -1) throw new Error('Book not found');
books[bookIndex] = {
...books[bookIndex],
title: title || books[bookIndex].title,
author: author || books[bookIndex].author,
};
return books[bookIndex];
},
deleteBook: (_, { id }) => {
const bookIndex = books.findIndex(book => book.id === id);
if (bookIndex === -1) throw new Error('Book not found');
books.splice(bookIndex, 1);
return true;
},
},
};
module.exports = resolvers;Set up the Express server with express-graphql in server.js:
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const schema = require('./schema');
const resolvers = require('./resolvers');
const app = express();
app.use('/graphql', graphqlHTTP({
schema,
// buildSchema expects rootValue functions to receive the field args as the
// first parameter (e.g. `book: ({id}) => ...`). Our `resolvers.js` follows
// the common resolver signature (parent, args, context, info). Wrap each
// resolver so buildSchema's calling convention maps to the existing code.
rootValue: (() => {
const wrap = fn => (...callArgs) => {
// buildSchema passes args as the first argument to root functions
const args = callArgs[0] || {};
return fn(null, args);
};
const root = {};
if (resolvers.Query) Object.keys(resolvers.Query).forEach(k => { root[k] = wrap(resolvers.Query[k]); });
if (resolvers.Mutation) Object.keys(resolvers.Mutation).forEach(k => { root[k] = wrap(resolvers.Mutation[k]); });
return root;
})(),
graphiql: true, // Enable GraphiQL interface
}));
const PORT = 4000;
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/graphql`);
});Start the server:
bash
node server.js
Open http://localhost:4000/graphql in your browser to access the GraphiQL interface.
Test these queries and mutations in GraphiQL:
Check all books to verify the array:
query {
books {
id
title
author
}
}Retry the single book query:
query {
book(id: "1") {
id
title
author
}
}Add a new book:
mutation {
addBook(title: "To Kill a Mockingbird", author: "Harper Lee") {
id
title
author
}
}Update a book:
mutation {
updateBook(id: "1", title: "The Great Gatsby Updated") {
id
title
author
}
}Delete a book:
mutation {
deleteBook(id: "1")
}