Designing a GraphQL-Interface on a NodeJS/Express-Backend

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")
}

Leave a Reply