|
1 | | -# Thinking in GraphQL exercise |
| 1 | +# GraphQL API Fundamentals |
2 | 2 |
|
3 | 3 | This exercise is part of the [React GraphQL Academy](http://reactgraphql.academy) learning material. The goal of the exercise is to help you get started transitioning from REST to GraphQL. |
4 | 4 |
|
5 | 5 | ## Learning objectives |
6 | 6 |
|
7 | | -- Thinking in Graphs |
8 | | -- Learn how to connect resolvers to a REST API |
9 | | -- Understand Schema Design principles |
| 7 | +- Understand the main functionalities and responsibilities of a GraphQL server |
| 8 | +- Learn how to migrate an existing REST API to GraphQL and start “thinking in graphs” |
| 9 | +- Start identifying potential problems when running real-world GraphQL APIs |
10 | 10 |
|
11 | 11 | ## Exercise part 1 |
12 | 12 |
|
13 | | -[https://rickandmortyapi.com/graphql/](https://rickandmortyapi.com/graphql/) |
| 13 | +[](TODO DEPLOY URL) |
14 | 14 |
|
15 | | -- Query a list with all the character names |
16 | | -- Query how many characters are in the system |
17 | | -- Query a single characther by id (try id equals 1) and get its name |
| 15 | +- Query a list with all the training titles |
| 16 | +- Query how many training are in the system? |
| 17 | +- Query a single training by id (try id equals TODO ADD ONE????????) and get its name |
18 | 18 | - How many types do we have in the system? |
19 | 19 |
|
20 | 20 | ## Exercise part 2 |
21 | 21 |
|
22 | 22 | ### To get started |
23 | 23 |
|
24 | | -We are going to create our own GraphQL API on top of this [Rick and Morty API](https://rickandmortyapi.com/documentation/#rest) |
| 24 | +We are going to create our own GraphQL API on top of this [REST API](https://mockedrestapi.reactgraphql.academy/) |
25 | 25 |
|
26 | | -- `git clone git@github.com:reactgraphqlacademy/rest-to-graphql-workshop.git` |
27 | | -- `cd rest-to-graphql-workshop` |
| 26 | +- `git clone https://github.com/reactgraphqlacademy/graphql-api-training.git` |
| 27 | +- `cd graphql-api-training` |
28 | 28 | - `yarn install` or `npm install` |
29 | 29 | - `yarn start` or `npm start` |
30 | 30 |
|
| 31 | +### Before we start |
| 32 | + |
| 33 | +- Clone the repo, git checkout the `fundamentals` branch, install the dependencies and let me walk you through the code meanwhile. |
| 34 | +- We use nodemon in the `start` script, so every time you save the server will restart automatically. |
| 35 | +- The `src/index.js` is the [getting started tutorial](https://www.apollographql.com/docs/apollo-server/getting-started/) from Apollo. |
| 36 | +- Let's replace the schema: |
| 37 | + |
| 38 | +```graphql |
| 39 | +type Query { |
| 40 | + books: [Book] |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +with |
| 45 | + |
| 46 | +```graphql |
| 47 | +type Query { |
| 48 | + avocados: [Book] |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +What do we need to change so the field avocados returns the array of books when we run the query? I'll give you 2 minutes to fix it :) |
| 53 | + |
31 | 54 | ### Tasks |
32 | 55 |
|
33 | | -- [ ] 1. Create a `Character` type in your schema. Use the [documentation of the character endpoint](https://rickandmortyapi.com/documentation/#character-schema) to define the shape of the `Character` type. |
| 56 | +⚠️ Some info before you start the tasks: |
| 57 | +1- You can define an array using square brackets and the type, example `[Book]` |
| 58 | +2- You can use the type `ID` for ids. |
| 59 | +3- In GraphQL types are nullable by default. If you want to make a type non-nullable use `!` (excalmation mark). Example: |
34 | 60 |
|
35 | | - - [ ] 1.1. Add a `characters` field to the `Query` type. You can replace the `books` field from Query type on line 32 with `characters` since we won't use books. The `characters` field in the `Query` type should return an array of [Character]. |
36 | | - - [ ] 1.2. Add a `characters` resolver to the Query's resolvers. You can replace the `books` field from Query type on line 40 with `characters` since we won't use books. You can return the mock characters array (which is in the scope and defined at the bottom of the file index.js) in the resolver function. |
37 | | - - [ ] 1.3 You should be able to manually test the `characters` query in Playground at [http://localhost:4000/](http://localhost:4000/) |
| 61 | +```graphql |
| 62 | +type Book { |
| 63 | + id: ID! |
| 64 | +} |
| 65 | +``` |
38 | 66 |
|
39 | | -- [ ] 2. Create an `Episode` type in your schema. Use the [documentation of the episode endpoint](https://rickandmortyapi.com/documentation/#episode-schema) to define the shape of the `Episode` type. Here you are practicing what you've learned on the previous task (1). |
| 67 | +To complete the tasks you'll use the mock data and helper functions that are at the bottom of the file `src/index.js`. |
40 | 68 |
|
41 | | - - [ ] 2.1. Add an `episodes` field to the `Query` type. The `episodes` field should return an array of [Episode] |
42 | | - - [ ] 2.2. Add an `episodes` resolver to the Query's resolvers. You can return the mock episodes array (which is in the scope and defined at the bottom of the file index.js) in the resolver function. |
43 | | - - [ ] 2.3 You should be able to manually test the `episodes` query in Playground at [http://localhost:4000/](http://localhost:4000/) |
| 69 | +- [ ] 1. Create a `Training` type in your schema. Define the following fields `title`, `id`, `objectives`, `curriculum`. Have a look at the mock training data in `src/index.js` to identify the types of each field. |
44 | 70 |
|
45 | | -- [ ] 3. Replace the mock data using https://rickandmortyapi.com/documentation/#rest. |
| 71 | + - [ ] 1.1. Add a `trainings` field to the `Query` type. You can replace the `books` field from Query type with `trainings` since we won't use books. The `trainings` field in the `Query` type should return an array of training. . |
| 72 | + - [ ] 1.2. Add a `trainings` resolver to the Query's resolvers. You can replace the `books` field from Query type with `trainings` since we won't use books. You can return the trainingMockData array (which is in the scope and defined at the bottom of the file index.js) in the resolver function. |
| 73 | + - [ ] 1.3 You should be able to manually test the `trainings` query in Playground at [http://localhost:4000/](http://localhost:4000/) |
46 | 74 |
|
47 | | - - You can use the `fetchEpisodes` and `fetchCharacters` defined at the bottom of this file `src/index.js` |
48 | | - - You'll need to replace mock data in 2 different places: |
49 | | - - Query characters |
50 | | - - Query episodes |
| 75 | +- [ ] 2. Create a `Discount` type in your schema. Define only the fields `code`, `id`, and `discountPercentage`. Have a look at the discount mock data in `src/index.js` to identify the types of each field. |
51 | 76 |
|
52 | | -- [ ] 4. Create a relationship between Episode type and Character type in your schema. Please have a look at the [documentation of the episode endpoint](https://rickandmortyapi.com/documentation/#episode-schema) to see how to get the episodes of a given character (heads up! we are calling the field in our Characters `episodes` but the REST API is calling the field that returns an array of episodes as `episode` - singular!). Hints: |
| 77 | + - [ ] 2.1. Add an `discounts` field to the `Query` type. The `discounts` field should return an array of discounts. |
| 78 | + - [ ] 2.2. Add an `discounts` resolver to the Query's resolvers. You can return the mock discounts array (which is in the scope and defined at the bottom of the file index.js) in the resolver function. |
| 79 | + - [ ] 2.3 You should be able to manually test the `discounts` query in Playground at [http://localhost:4000/](http://localhost:4000/) |
53 | 80 |
|
54 | | - - You need to add a `Character` key in the resolvers object and an object with an `episodes` key in `Character`. Similar to the Author type and books field in the [Apollo documentation](https://www.apollographql.com/docs/apollo-server/essentials/data#resolver-map). Hint: The first argument of the resolver is the 'parent' type, in this case, the parent of the `episodes` field is the `Character`. parent.episode gives you the array of episodes returned from the REST API. |
55 | | - - You can use the helper fetch functions defined at the bottom of this file `src/index.js`. |
| 81 | +- [ ] 3. Replace the mock data with real data using the following endpoints: |
| 82 | + - [https://mockedrestapi.reactgraphql.academy/v1/trainings](https://mockedrestapi.reactgraphql.academy/v1/trainings) |
| 83 | + - [https://mockedrestapi.reactgraphql.academy/v1/discounts](https://mockedrestapi.reactgraphql.academy/v1/discounts) |
56 | 84 |
|
57 | | -- [ ] 5. Create a query that returns a single Character given an id. You need to fetch the character using `https://rickandmortyapi.com/documentation/#get-a-single-character`. Hint, you need to use [arguments](https://graphql.org/graphql-js/passing-arguments/) |
| 85 | +Hint. You can use the `fetchTrainings` and `fetchDiscounts` defined at the bottom of this file `src/index.js` |
58 | 86 |
|
59 | | -### Bonus |
| 87 | +- You'll need to replace mock data in 2 different places: |
| 88 | + - Query discounts |
| 89 | + - Query trainings |
60 | 90 |
|
61 | | -- Create the types and resolvers so the following query works: |
| 91 | +Note on mocking. In the next session we'll use the automocking feature of Apollo Server. The only thing you need to do is `mocks:true` in your Apollo Server configuration. More info [here](https://www.apollographql.com/docs/apollo-server/testing/mocking/). |
62 | 92 |
|
| 93 | +```js |
| 94 | +const server = new ApolloServer({ |
| 95 | + typeDefs, |
| 96 | + mocks: true // ⬅️⬅️⬅️⬅️ |
| 97 | +}); |
63 | 98 | ``` |
64 | | -query episode { |
65 | | - episode(id: 1) { |
| 99 | + |
| 100 | +#### 🏋️♀️ Bonus exercise part 2 |
| 101 | + |
| 102 | +Congratulations, you've completed part 2! You've learned how to create object types in GraphQL and add fields to them using scalar types (`String`,`Int`, `ID`) or your own object types (`Training`, `Discount`). You've also learned how to create a relationship between two object types. |
| 103 | + |
| 104 | +In GraphQL you can also create your custom scalars types, like [Date](https://graphql.org/learn/schema/#scalar-types). |
| 105 | + |
| 106 | +Bonus task, add a field called `startDate` to the `Training` object type using a `DateTime` scalar type. GraphQL doesn't provide a DateTime type. Instead of creating a custom `DateTime` scalar type you are going to use [https://github.com/excitement-engineer/graphql-iso-date](https://github.com/excitement-engineer/graphql-iso-date). Note, the package is already installed in package.json. |
| 107 | + |
| 108 | +## Exercise part 3 |
| 109 | + |
| 110 | +### Before we start |
| 111 | + |
| 112 | +Resolvers are functions that have 4 arguments `(parent, args, context, info)`. In this exercise, we are only going to use the first 2 arguments: `parent` and `args`. |
| 113 | + |
| 114 | +#### The first argument of the resolver |
| 115 | + |
| 116 | +The first argument, often called `parent`, points to the parent object. For instance, we could override the default resolver of the title field in the Training and return an upper case version of the title. |
| 117 | + |
| 118 | +⚠️ Trainer implements: |
| 119 | + |
| 120 | +```js |
| 121 | +const resolvers = { |
| 122 | + Query: { |
| 123 | + //... |
| 124 | + }, |
| 125 | + Training: { |
| 126 | + title: (parent) { |
| 127 | + return parent.title.toUpperCase() |
| 128 | + } |
| 129 | + } |
| 130 | +}; |
| 131 | +``` |
| 132 | + |
| 133 | +We could also create a new field that returns the upper case version of the title without changing the title field. Example: |
| 134 | + |
| 135 | +⚠️ Learners implement (only 5 minutes to implement and write a query to test it!): |
| 136 | + |
| 137 | +```graphql |
| 138 | +type Training { |
| 139 | + title: String! |
| 140 | + upperCaseTitle: String! |
| 141 | + # the rest remains the same |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +```js |
| 146 | +const resolvers = { |
| 147 | + Query: { |
| 148 | + //... |
| 149 | + }, |
| 150 | + Training: { |
| 151 | + upperCaseTitle: (parent) { |
| 152 | + return parent.title.toUpperCase() |
| 153 | + } |
| 154 | + } |
| 155 | +}; |
| 156 | +``` |
| 157 | + |
| 158 | +🏋️♀️Bonus exercise, return all the URLs of the discounts field in upper case. |
| 159 | + |
| 160 | +#### The second argument of the resolver |
| 161 | + |
| 162 | +The second argument of the resolver (we are calling it `args`) points to the arguments passed to the field. In the following example `args` contains `id`: |
| 163 | + |
| 164 | +```js |
| 165 | +const schema = gql` |
| 166 | + type Query { |
| 167 | + author(id: ID!): Author |
| 168 | + } |
| 169 | +`; |
| 170 | +const resolvers = { |
| 171 | + Query: { |
| 172 | + author(parent, args) { |
| 173 | + console.log(args); // { id: 3 } based on the query below |
| 174 | + } |
| 175 | + } |
| 176 | +}; |
| 177 | +``` |
| 178 | + |
| 179 | +```graphql |
| 180 | +query authorName { |
| 181 | + author(id: 3) { |
66 | 182 | name |
67 | | - characters { |
68 | | - name |
| 183 | + } |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +### Tasks |
| 188 | + |
| 189 | +To complete the tasks you'll use the helper functions that are at the bottom of the file `src/index.js` |
| 190 | + |
| 191 | +- [ ] 4. Implement a new field in the `Query` type that returns a single training given an id. You need to fetch the training from this endpoint `https://restapi.reactgraphql.academy/v1/trainings/` + `id`. Hint, you need to pass [arguments](https://graphql.org/graphql-js/passing-arguments/) to the field, and then use the second argument in the resolver. There is a helper function at the bottom of `src/index.js`. |
| 192 | + |
| 193 | +Once implemented you should be able to run the following query: |
| 194 | + |
| 195 | +```graphql |
| 196 | +query getTraining { |
| 197 | + training(id: "tra:22") { |
| 198 | + title |
| 199 | + } |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +- [ ] 5. Create the following relationship between the Training type and the Discount type in your schema. |
| 204 | + |
| 205 | +```graphql |
| 206 | +type Training { |
| 207 | + discounts: [Discount] |
| 208 | + # the rest of the fields remain the same |
| 209 | +} |
| 210 | +``` |
| 211 | + |
| 212 | +- You need to add a `Training` key in the resolvers object and an object with an `discounts` key in `Training`. Similar to the Author type and books field in the [Apollo documentation](https://www.apollographql.com/docs/apollo-server/essentials/data#resolver-map) |
| 213 | +- You need to use the **first argument of the resolver**: the 'parent'. In this case, the parent of the `discounts` field is the `Training`. `parent.discounts` gives you the array of URLs that you can use to fetch each discount from the REST API. |
| 214 | +- You can use the helper function `fetchDiscountByUrl` defined at the bottom of this file `src/index.js`. |
| 215 | +- Heads up! We want our Training type to have a field called `discounts` that returns an array of `Discount` types not an array of `String` |
| 216 | +- Once implemented, you should be able to run the following query: |
| 217 | + |
| 218 | +```graphql |
| 219 | +query getTraining { |
| 220 | + training(id: "tra:22") { |
| 221 | + title |
| 222 | + discounts { |
| 223 | + code |
69 | 224 | } |
70 | 225 | } |
71 | 226 | } |
72 | 227 | ``` |
73 | 228 |
|
74 | | -- Once implemented, do you see any vulnerability issues on that query? |
| 229 | +#### 🏋️♀️ Bonus exercise part 3 |
| 230 | + |
| 231 | +- Create the types and resolvers so the following query works: |
| 232 | + |
| 233 | +```graphql |
| 234 | +query getDangerousDiscount { |
| 235 | + discount(id: "dis:421") { |
| 236 | + code |
| 237 | + training { |
| 238 | + title |
| 239 | + discounts { |
| 240 | + code |
| 241 | + # why this query could be dangerous? |
| 242 | + } |
| 243 | + } |
| 244 | + } |
| 245 | +} |
| 246 | +``` |
| 247 | + |
| 248 | +Once implemented, do you see any problems/ vulnerability issues on that query? |
| 249 | + |
| 250 | +## Homework |
| 251 | + |
| 252 | +You are going to build a GraphQL API on top of an existing REST API. Steps: |
| 253 | + |
| 254 | +1- Choose a public API. You have a list of public APIs [here](https://github.com/public-apis/public-apis). Suggestion, choose an API that doesn't require authentication and has decent documentation. |
| 255 | + |
| 256 | +2- Create a GraphQL server to validate and execute the GraphQL queries. You can get started using the [getting started tutorial](https://www.apollographql.com/docs/apollo-server/getting-started/) from Apollo Server. |
| 257 | + |
| 258 | +3- Create the GraphQL schema using the [Schema Definition Language (SDL)](https://www.prisma.io/blog/graphql-sdl-schema-definition-language-6755bcb9ce51) . You'll define types and relationships between those types. |
| 259 | + |
| 260 | +4- Add the resolvers to your schema. We are following a SDL-first approach to build our schema. It's the most popular approach in the GraphQL JavaScript community, but be aware that it's not the only one. You can read more about it and other alternatives in this [article](https://www.prisma.io/blog/the-problems-of-schema-first-graphql-development-x1mn4cb0tyl3). |
75 | 261 |
|
76 | 262 | ## Articles and links |
77 | 263 |
|
|
0 commit comments