Search code examples
graphqllogicapolloserver-sideapollo-server

What is the design pattern for doing Apollo serverside logic


To learn GraphQL and Apollo I am working on a Tic Tac Toe game. Currently the way it works is that the server stores all game information so when the client clicks a square, it sends a mutation to the server which updates the GameState type and returns the new state. What I would like is for the server to handle all game logic like checking if a move is legal, playing the computers move, checking for winners, and then returning the new GameState.

My initial thought is to just put all that logic in the mutation resolver but in the tutorial I followed it strongly recommended keeping resolvers as small as possible so that does not seem like the best design. I cannot think of another way to do it though, are Apollo servers generally not used this way?

Potentially relevant code:

Schema:

const typeDefs = gql`
    type GameState {
        board: [[String!]!]!
        status: String!
        winner: String
    }
    type Mutation {
        placeO(x: Int!, y: Int!): GameState
    }
    type Query {
        "Query to get tracks array for the homepage grid"
        getGameState: GameState
    }
`;

Mutation Resolver:

  Mutation: {
    placeO: (_, { x, y }, { dataSources }) => {
      // check_legal_move(x, y, dataSources.state.board); ??
      dataSources.state.board[x][y] = 'O';
      // computer_move(dataSources.state.board); ??
      return dataSources.state;
    }
  },

Client Mutation Request:

const PLACE_O = gql`
    mutation placeO($x: Int!, $y: Int!) {
        placeO(x: $x, y: $y) {
            board,
            status,
            winner,
        }
    }
`;

Solution

  • it strongly recommended keeping resolvers as small as possible

    What it means is this: as a general rule, the GraphQL layer (as with any API implementation layer) should be treated as a "translator". It should:

    1. Accept the incoming GraphQL input
    2. Convert it to a function call
    3. Call the function
    4. Accept the function output
    5. Convert it into GraphQL output
    6. Return GraphQL Output

    In your example, that could look something like this:

    const resolvers = {
      Mutation: {
        placeO: (_, { x, y }, { dataSources }) => {
          return dataSources.game.placeMark({ x, y, value: 'O' });
        }
      },
    }
    

    Instead of your dataSource being state, it should be where you do your business logic, and it will control what happens from there. The dataSource would have something like what you've got in your resolver

    {
      placeMark({ x, y, value }) {
        const state = this.getState();
        // check_legal_move(x, y, state.board); ??
        state.board[x][y] = 'O';
        // computer_move(state.board); ??
        return state;
      }
    }
    

    But Daaan, why shouldn't I just do this in the resolver?

    Good question. One answer to this is the Single Responsibility Principle, which would frown on you doing routing, business logic, and state management all in the same place. While following this principle isn't always "the most efficient", "the best answer", or even in rare cases "a correct answer", knowing when to use this pattern in application design will treat you well throughout your career.

    The biggest value I've seen with it with GraphQL is that it helps with the fact that GraphQL is, well "a Graph". The things you do, the data you represent are all just nodes in a sea of connections, and you will add new connections, delete old ones, deprecate things, repoint a line from node A to node B so that now it goes to Node C, and you don't want to have to worry about either a) breaking your existing API or b) copy/pasting all of your business logic.

    Let's say you want to make a generic mutation so that you don't have to do Xs and Os separately. Without breaking anything, you can add:

    const resolvers = {
      Mutation: {
        placeO: (_, { x, y }, { dataSources }) => {
          return dataSources.game.placeMark({ x, y, value: 'O' });
        }
        placeMark: (_, { x, y, value }, { dataSources }) => {
          return dataSources.game.placeMark({ x, y, value });
        }
      },
    }