3. Local development of a GraphQL static web app

In this article, learn how to locally build a static web app and API that uses the Apollo GraphQL client and server libraries.

Fork the sample GitHub repo

Because static web apps deploy from a GitHub repo and you need to be able to push your changes to that repo, this section helps you create your own repo.

  1. Open the sample repo in a browser: https://github.com/azure-samples/js-e2e-graphql-cosmosdb-static-web-app.

  2. Fork the repository into your own account by selecting Fork.

    Partial screenshot of browser showing the sample GitHub repository, with the Fork button highlighted.

  3. In a bash terminal on your local machine, clone your fork. Replace YOUR-ACCOUNT with your own account name.

    git clone https://github.com/YOUR-ACCOUNT/js-e2e-graphql-cosmosdb-static-web-app
    
  4. In a bash terminal, install the dependencies.

    cd "js-e2e-graphql-cosmosdb-static-web-app" && \
    npm install && \
    cd api && \
    npm install && \
    cd .. 
    
  5. Open the project in Visual Studio Code.

    code .
    

Create a container in the emulator

In a local container created with Azure Cosmos DB, you can use the Azure Cosmos DB emulator, which allows you to develop your application without creating or using a cloud-based resource.

  1. Start your local Azure Cosmos DB emulator, if it isn't already running.

  2. Select New Container.

  3. In the side panel, enter the following settings:

    Setting Value
    Database ID trivia
    Container ID game
    Partition key modelType

    Accept the defaults for all other values.

    Screenshot of Azure Cosmos DB emulator, showing how to create a new container.

  4. Select OK to finish the local database creation process.

Load the JSON file into a local emulator container

Load the 100 trivia questions into the container.

  1. In the local Azure Cosmos DB emulator, select the trivia database, then the gamecontainer, and then Items.

  2. Select Upload item, select the folder icon in the side panel, and then select the location for the ./api/trivia.json file. Then select Upload.

    Partial screenshot of browser showing the UI elements to select to upload the JSON file.

  3. Refresh the container to see the 100 items with the modelType of Question.

    An example of one of the questions in the container is:

    {
        "id": "0",
        "category": "Science: Computers",
        "type": "multiple",
        "difficulty": "easy",
        "question": "What does CPU stand for?",
        "correct_answer": "Central Processing Unit",
        "incorrect_answers": [
            "Central Process Unit",
            "Computer Personal Unit",
            "Central Processor Unit"
        ],
        "modelType": "Question",
        "_rid": "t1EcAJE92MQBAAAAAAAAAA==",
        "_self": "dbs/t1EcAA==/colls/t1EcAJE92MQ=/docs/t1EcAJE92MQBAAAAAAAAAA==/",
        "_etag": "\"00000000-0000-0000-7e5b-22dca8c401d7\"",
        "_attachments": "attachments/",
        "_ts": 1626890792
    }
    

Configure the local API to connect to the local Azure Cosmos DB emulator

  1. In the browser window for the emulator, https://localhost:8081/_explorer/index.html, copy the Primary Connection String.

    Partial screenshot of browser showing the local Azure Cosmos DB emulator, with the Primary Connection String highlighted.

  2. Paste the value into the ./api/local.settings.json file, for the CosmosDB property.

    {
      "IsEncrypted": false,
      "Values": {
        "FUNCTIONS_WORKER_RUNTIME": "node",
        "AzureWebJobsStorage": "",
        "CosmosDB": "PASTE-CONNECTION-STRING-HERE"
      }
    }
    

Build and run the local static web app

Both the client app and the Azure Functions API need to be started. The client runs on port 3000, the Functions API runs on port 7071, and the emulator runs on port 8081.

  1. In a Visual Studio Code integrated terminal, build and run the Functions API:

    cd api && npm start
    

    The API has to generate the TypeScript types and the GraphQL schema files, and then start the HTTP endpoint.

  2. In a separate Visual Studio Code integrated terminal, build and run the client React app:

    npm start
    
  3. When your local browser opens to the client, http://localhost:3000/, open the browser's developer tools (press F12) so you can see the HTTP request and response from the Functions API.

    Both the client and the API use Apollo GraphQL libraries to help construct and process GraphQL queries.

Start a new trivia game in a web browser

The game selects ten random questions for you to answer. Each question is timed. Try to answer as quickly as possible. All trivia games you complete in a browser session are tied to your user name.

  1. In the browser, select Start a new game.

    Partial screenshot of browser showing the Start a new game button.

  2. Enter your name, and select Join the game.

    Partial screenshot of browser showing the textbox where you enter your name, and the Join a game button.

  3. In the browser developer tools, view the request payload to http://localhost:3000/api/graphql to see the GraphQL query:

    {
        "operationName":"CreateGame",
        "variables":{},
        "query":"mutation CreateGame {
            createGame {
                id
                __typename
            }
        }"
    }
    

    The preceding JSON has been cleaned up for readability. The CreateGame in the query maps directly to the createGame mutation in ./api/resolvers.ts.

    async createGame(_, __, { dataSources }) {
      const questions = await dataSources.question.getQuestions();
      const game = await dataSources.game.createGame(questions);
    
      return game;
    },
    
  4. The GraphQL API responds with the following JSON:

    {
        "data": {
            "createGame":{
                "id":"nuug",
                "__typename":"Game"
            }
        }
    }
    
  5. Immediately after that, without further input from the user, the React client makes another call to the API to add the user. The client uses a GraphQL query, seen in the browser's developer tools:

    {
        "operationName":"addPlayerScreen",
        "variables":{
            "id":"nuug",
            "name":"Dina"
        },
        "query":"mutation addPlayerScreen($id: ID!, $name: String!) {
            addPlayerToGame(id: $id, name: $name) {
                id
                __typename
            }
            startGame(id: $id) {
                id
                players {
                    id
                    name
                    __typename
                }
                __typename
            }
        }"
    }
    

    The preceding JSON has been cleaned up for readability. This GraphQL query has two requests: addPlayerToGame, and startGame, which maps directly to mutations in ./api/resolvers.ts.

    async addPlayerToGame(_, { id, name }, { dataSources }) {
      const user = await dataSources.user.createUser(name);
      const game = await dataSources.game.getGame(id);
      game.players.push(user);
      await dataSources.game.updateGame(game);
    
      return user;
    },
    
    async startGame(_, { id }, { dataSources }) {
      const game = await dataSources.game.getGame(id);
      game.state = GameState.Started;
      return await dataSources.game.updateGame(game);
    },
    
  6. The GraphQL API responds with the following JSON:

    {
        "data": {
            "addPlayerToGame":{
                "id":"rfxb",
                "__typename":"Player"
            },
            "startGame":{
                "id":"nuug",
                "players":
                [
                    {
                        "id":"rfxb",
                        "name":"Dina",
                        "__typename":"Player"
                    }
                ],
                "__typename":"Game"
            }
        }
    }
    

React client fetches game trivia from the GraphQL API

  1. After you create the user and game, you use this browser request to get the game trivia:

    {
        "operationName":"getGame",
        "variables":{
            "id":"nuug"
        },
        "query":"query getGame($id: ID!) {
            game(id: $id) {
                questions {
                    id
                    question
                    answers
                    __typename
                }
                __typename
            }
        }"
    }
    

    The preceding JSON has been cleaned up for readability. This GraphQL query, getGame, maps to the game query in ./api/resolvers.ts.

    game(_, { id }, { dataSources }) {
      return dataSources.game.getGame(id);
    },
    
  2. The GraphQL API responds with the following JSON:

    {
        "data": {
            "game":{
                "questions":[
                    {
                        "id":"34",
                        "question":"How many values can a single byte represent?",
                        "answers":["1024","256","1","8"],
                        "__typename":"Question"
                    },
                    ...remaining array elements removed for brevity...
                ],
                "__typename":"Game"
            }
        }
    }
    

    Notice that the correct answer isn't returned in the dataset.

  3. When you select an answer for a question, the request returns that to the Functions API:

    {
        "operationName":"submitAnswer",
        "variables":{
            "gameId":"nuug",
            "playerId":"rfxb",
            "questionId":"64",
            "answer":""
        },
        "query":"mutation submitAnswer(
                $gameId: ID!, 
                $playerId: ID!, 
                $questionId: ID!, 
                $answer: String!
            ) {
            submitAnswer(
                gameId: $gameId 
                playerId: $playerId
                questionId: $questionId
                answer: $answer
            ) {
                id
                __typename
            }
        }"
    }
    

    The preceding JSON has been cleaned up for readability. This GraphQL mutation, submitAnswer, maps to the submitAnswer mutation in ./api/resolvers.ts.

    async submitAnswer(
      _,
      { answer, gameId, playerId, questionId },
      { dataSources }
    ) {
      const [game, user, question] = await Promise.all([
        dataSources.game.getGame(gameId),
        dataSources.user.getUser(playerId),
        dataSources.question.getQuestion(questionId),
      ]);
    
      const answerModel: UserAnswerModel = {
        id: `${gameId}-${questionId}-${playerId}`,
        modelType: ModelType.UserAnswer,
        answer,
        question,
        user,
      };
    
      game.answers.push(answerModel);
    
      await dataSources.game.updateGame(game);
      return user;
    },
    
  4. The GraphQL API responds with the following JSON:

    {
        "data":{
            "submitAnswer":{
                "id":"rfxb",
                "__typename":"Player"
            }
        }
    }
    

React client fetches game results from GraphQL API

  1. When the game is complete, the client requests the results of the game:

    {
        "operationName":"playerResults",
        "variables":{
            "gameId":"nuug",
            "playerId":"rfxb"
        },
        "query":"query playerResults(
            $gameId: ID!, 
            $playerId: ID!
        ) {
            playerResults(
                gameId: $gameId, 
                playerId: $playerId
            ) {
                correct
                question
                answers
                correctAnswer
                submittedAnswer
                __typename
            }
        }"
    }
    

    The preceding JSON has been cleaned up for readability. This GraphQL mutation, playerResults maps to the playerResults query in ./api/resolvers.ts.

    async playerResults(_, { gameId, playerId }, { dataSources }) {
      const game = await dataSources.game.getGame(gameId);
    
      const playerAnswers = game.answers.filter((a) => a.user.id === playerId);
    
      return playerAnswers.map((answer) => {
        const question = answer.question;
        return {
          name: answer.user.name,
          answers: arrayRandomiser(
            question.incorrect_answers.concat(question.correct_answer)
          ),
          question: question.question,
          correctAnswer: question.correct_answer,
          submittedAnswer: answer.answer,
          correct: answer.answer === question.correct_answer,
        };
      });
    },
    
  2. The GraphQL API responds with the following JSON:

    {
        "data":{
            "playerResults":[
                {
                    "correct":false,
                    "question":"What was Frank West's job in "Dead Rising"?",
                    "answers":["Photojournalist","Chef","Taxi Driver","Janitor"],
                    "correctAnswer":"Photojournalist",
                    "submittedAnswer":"",
                    "__typename":"PlayerResult"
                },
                ...remaining array elements removed for brevity...
            ]
        }
    }
    
  3. This allows the React client to display the game results.

    Partial screenshot of browser showing the game results in the React client app.

View the data in the emulator

  1. Return to the local Azure Cosmos DB emulator, http://localhost:8081/, and edit the container filter to query for the game results by using the game ID.

    SELECT * FROM c WHERE c.id = "REPLACE-WITH-YOUR-GAME-ID"
    
  2. Apply the filter to see the results.

    Partial screenshot of browser showing the game results in the emulator.

Translate GraphQL queries to Azure Cosmos DB queries

This implementation of GraphQL doesn't automatically map the GraphQL queries to the game container. You as the application developer have to provide those database queries.

The Functions API in the sample project provides the Azure Cosmos DB table queries in the ./api/graphql/data/cosmos files. The functionality aligns to the file names:

Each file provides the Azure Cosmos DB query functions that are called by the GraphQL resolvers. For example, to get the game results for a player, the playerResults GraphQL query is called. This query:

  1. Gets the game details from Azure Cosmos DB (const game = await dataSources.game.getGame(gameId);).
  2. Gets the player answers from the database response (const playerAnswers = game.answers.filter((a) => a.user.id === playerId);).
  3. Determines if the answers were correct.

The getGame functionality requires a call to the Azure Cosmos DB data source with the corresponding query:

async getGame(id: string) {
  const game = await this.findManyByQuery({
    query: "SELECT TOP 1 * FROM c WHERE c.id = @id AND c.modelType = @type",
    parameters: [
      { name: "@id", value: id },
      { name: "@type", value: ModelType.Game },
    ],
  });

  return game.resources[0];
}

Troubleshooting

The most common reasons this doesn't work locally are:

  • Both the client and API aren't running. Make sure that both endpoints are available from a browser:

    • http://localhost:3000 - React client
    • http://locahost:7071/api/graphql - GraphQL Functions API
  • The database and container aren't created or named, trivia and game.

  • The container doesn't have the ./api/trivia.json data loaded.

If you run into an error that isn't listed above, open an issue on this article with your error, and include the steps leading up to the problem.

Next steps