Episode 9 of 9

Update Mutation

Learn how to build update mutations to modify existing records, handle partial updates, and manage edit workflows.

Update Mutation

In the previous episode, we learned how to add and delete data. Now let's complete the CRUD picture with update mutations — modifying existing records.

Defining the Update Schema

For updates, we want to allow partial updates — only changing the fields that are provided. We create an input type where all fields are optional:

type Mutation {
  addGame(game: AddGameInput!): Game
  deleteGame(id: ID!): [Game]
  updateGame(id: ID!, edits: EditGameInput!): Game
}

input AddGameInput {
  title: String!
  platform: [String!]!
}

input EditGameInput {
  title: String       # Optional — only update if provided
  platform: [String!] # Optional — only update if provided
}

Notice the difference:

Input TypeFieldsPurpose
AddGameInputAll required (!)Creating new records
EditGameInputAll optional (no !)Partial updates

Writing the Update Resolver

const resolvers = {
  Mutation: {
    // ... other mutations ...

    updateGame(_, args) {
      // Find the game to update
      const gameIndex = games.findIndex(g => g.id === args.id);

      if (gameIndex === -1) {
        throw new Error('Game not found');
      }

      // Merge existing data with provided edits
      games[gameIndex] = {
        ...games[gameIndex],  // Keep existing fields
        ...args.edits,        // Override with provided edits
      };

      return games[gameIndex];
    },
  },
};

How Spread Merging Works

The magic is in the spread operator. If a user only provides a title:

// Existing game
{ id: "1", title: "Zelda", platform: ["Switch"] }

// args.edits = { title: "Zelda: TotK" }

// After spread merge:
{
  ...existingGame,    // id: "1", title: "Zelda", platform: ["Switch"]
  ...args.edits,      // title: "Zelda: TotK"
}
// Result: { id: "1", title: "Zelda: TotK", platform: ["Switch"] }
// Only the title changed! Platform stayed the same.

Running the Update Mutation

Update just the title:

mutation UpdateGame($id: ID!, $edits: EditGameInput!) {
  updateGame(id: $id, edits: $edits) {
    id
    title
    platform
  }
}
{
  "id": "1",
  "edits": {
    "title": "Zelda: Tears of the Kingdom"
  }
}

Update just the platform:

{
  "id": "2",
  "edits": {
    "platform": ["PS5", "PC", "Xbox"]
  }
}

Update both fields:

{
  "id": "3",
  "edits": {
    "title": "Elden Ring: Shadow of the Erdtree",
    "platform": ["PS5", "Xbox", "PC"]
  }
}

Complete Mutations for All Types

type Mutation {
  # Game mutations
  addGame(game: AddGameInput!): Game
  updateGame(id: ID!, edits: EditGameInput!): Game
  deleteGame(id: ID!): [Game]

  # Review mutations
  addReview(review: AddReviewInput!): Review
  updateReview(id: ID!, edits: EditReviewInput!): Review
  deleteReview(id: ID!): [Review]
}

input AddGameInput {
  title: String!
  platform: [String!]!
}

input EditGameInput {
  title: String
  platform: [String!]
}

input AddReviewInput {
  rating: Int!
  content: String!
  game_id: ID!
  author_id: ID!
}

input EditReviewInput {
  rating: Int
  content: String
}

Full Resolver Implementation

const resolvers = {
  Query: {
    games: () => games,
    game: (_, args) => games.find(g => g.id === args.id),
    reviews: () => reviews,
    review: (_, args) => reviews.find(r => r.id === args.id),
    authors: () => authors,
    author: (_, args) => authors.find(a => a.id === args.id),
  },

  Game: {
    reviews: (parent) => reviews.filter(r => r.game_id === parent.id),
  },

  Review: {
    game: (parent) => games.find(g => g.id === parent.game_id),
    author: (parent) => authors.find(a => a.id === parent.author_id),
  },

  Author: {
    reviews: (parent) => reviews.filter(r => r.author_id === parent.id),
  },

  Mutation: {
    addGame: (_, args) => {
      const game = { id: String(games.length + 1), ...args.game };
      games.push(game);
      return game;
    },
    updateGame: (_, args) => {
      const idx = games.findIndex(g => g.id === args.id);
      if (idx === -1) throw new Error("Game not found");
      games[idx] = { ...games[idx], ...args.edits };
      return games[idx];
    },
    deleteGame: (_, args) => {
      games = games.filter(g => g.id !== args.id);
      return games;
    },
    addReview: (_, args) => {
      const review = { id: String(reviews.length + 1), ...args.review };
      reviews.push(review);
      return review;
    },
    updateReview: (_, args) => {
      const idx = reviews.findIndex(r => r.id === args.id);
      if (idx === -1) throw new Error("Review not found");
      reviews[idx] = { ...reviews[idx], ...args.edits };
      return reviews[idx];
    },
    deleteReview: (_, args) => {
      reviews = reviews.filter(r => r.id !== args.id);
      return reviews;
    },
  },
};

Course Summary 🎉

Congratulations! You've completed the GraphQL Crash Course! Here's everything you've learned:

  1. What GraphQL is — a query language that solves REST's problems
  2. Query basics — fields, nested queries, arguments
  3. Apollo Server — building a GraphQL server from scratch
  4. Schema & types — SDL, scalars, objects, enums
  5. Resolvers — connecting schema to data
  6. Query variables — dynamic, reusable queries
  7. Related data — modeling and resolving relationships
  8. Mutations — creating and deleting data
  9. Update mutations — partial updates with edit input types

Where to Go Next

  • Subscriptions — real-time data with WebSockets
  • Apollo Client — using GraphQL in React/Vue/Angular
  • DataLoader — solving the N+1 query problem
  • Authentication — securing your GraphQL API
  • Database integration — connecting to PostgreSQL, MongoDB, etc.

Happy coding! 🚀