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.
Open the sample repo in a browser: https://github.com/azure-samples/js-e2e-graphql-cosmosdb-static-web-app.
Fork the repository into your own account by selecting Fork.
In a bash terminal on your local machine, clone your fork. Replace
YOUR-ACCOUNTwith your own account name.git clone https://github.com/YOUR-ACCOUNT/js-e2e-graphql-cosmosdb-static-web-appIn a bash terminal, install the dependencies.
cd "js-e2e-graphql-cosmosdb-static-web-app" && \ npm install && \ cd api && \ npm install && \ cd ..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.
Start your local Azure Cosmos DB emulator, if it isn't already running.
Select New Container.
In the side panel, enter the following settings:
Setting Value Database ID triviaContainer ID gamePartition key modelTypeAccept the defaults for all other values.
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.
In the local Azure Cosmos DB emulator, select the
triviadatabase, then thegamecontainer, and then Items.Select Upload item, select the folder icon in the side panel, and then select the location for the
./api/trivia.jsonfile. Then select Upload.Refresh the container to see the 100 items with the
modelTypeofQuestion.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
In the browser window for the emulator,
https://localhost:8081/_explorer/index.html, copy the Primary Connection String.Paste the value into the
./api/local.settings.jsonfile, for theCosmosDBproperty.{ "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.
In a Visual Studio Code integrated terminal, build and run the Functions API:
cd api && npm startThe API has to generate the TypeScript types and the GraphQL schema files, and then start the HTTP endpoint.
In a separate Visual Studio Code integrated terminal, build and run the client React app:
npm startWhen 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.
In the browser, select Start a new game.
Enter your name, and select Join the game.
In the browser developer tools, view the request payload to
http://localhost:3000/api/graphqlto see the GraphQL query:{ "operationName":"CreateGame", "variables":{}, "query":"mutation CreateGame { createGame { id __typename } }" }The preceding JSON has been cleaned up for readability. The
CreateGamein the query maps directly to thecreateGamemutation in./api/resolvers.ts.async createGame(_, __, { dataSources }) { const questions = await dataSources.question.getQuestions(); const game = await dataSources.game.createGame(questions); return game; },The GraphQL API responds with the following JSON:
{ "data": { "createGame":{ "id":"nuug", "__typename":"Game" } } }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, andstartGame, 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); },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
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 thegamequery in./api/resolvers.ts.game(_, { id }, { dataSources }) { return dataSources.game.getGame(id); },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.
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 thesubmitAnswermutation 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; },The GraphQL API responds with the following JSON:
{ "data":{ "submitAnswer":{ "id":"rfxb", "__typename":"Player" } } }
React client fetches game results from GraphQL API
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,
playerResultsmaps to theplayerResultsquery 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, }; }); },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... ] } }This allows the React client to display the game results.
View the data in the emulator
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"Apply the filter to see the results.
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:
- Gets the game details from Azure Cosmos DB (
const game = await dataSources.game.getGame(gameId);). - Gets the player answers from the database response (
const playerAnswers = game.answers.filter((a) => a.user.id === playerId);). - 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 clienthttp://locahost:7071/api/graphql- GraphQL Functions API
The database and container aren't created or named,
triviaandgame.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.