Lauro SilvaLauro Silva

Instructor: Vladimir Novick

Workshop Repo: https://github.com/vnovick/moving-from-rest-to-graphql

Exercise 1 - exploring project REST api

  • REST generates a lot of requests. Under-feching. Not enough data to show in the page.
  • One of the biggest drawbacks of REST APIs is that they don’t have a schema describing what the data structures that are returned by the API endpoints look like.
  • You don’t need all the data on each request, this is called over-feching. Only solution is to increase complexity (painful process).
    • Over-fetching is fetching too much data, meaning there is data in the response you don’t use.
    • Under-fetching is not having enough data with a call to an endpoint, forcing you to call a second endpoint.
    • In both cases, they are performance issues: you either use more bandwidth than you should, or you are making more HTTP requests than you should.

Exercise 2 - Explore GraphQL queries and mutations

  • GraphQL gives you just one end point, where you send only Post requests.
  • https://graphiql-online.com/
  • GraphQL on the server is implemented in a Type System.
  • Mutations: mutation field returns an object type, you can ask for nested fields. This can be useful for fetching the new state of an object after an update. Let’s look at a simple example mutation
  • Insert both authors and posts in the same mutation and make sure both are inserted.
1{
2 posts {
3 author {
4 name
5 }
6 title
7 }
8}

Exercise 3 - GraphQL endpoint setup

  • Create reusable insert mutation called addPost from that mutation using variable of some input type.
    • Run playground: http://localhost:3001/graphql
1query getPostByTitle($title: String) {
2 posts(order_by: { timestamp: asc }, where: { title: { _eq: $title } }) {
3 title
4 }
5}
  • The first thing you need to understand is the shape of the data that’s returned by the different REST endpoints.
1// GraphQL Schema definition by defining root level Query type with one single hello query that will return a String
2const typeDefs = gql`
3 type Query {
4 hello: String
5 }
6`;
7
8// resolver for hello query that will return "Hello GraphQL"
9const resolvers = {
10 Query: {
11 hello: () => 'Hello GraphQL',
12 },
13};
14
15
16// a new ApolloServer providing it type definitions just defined
17// add mocks: true to ApolloServer to make sure we are able to get unimplemented resolvers through mocks.
18// Make sure to also add mockEntireSchema: false
19const server = new ApolloServer({typeDefs, resolvers, mocks: true, mockEntireSchema: false})

Exercise 4 - Design GraphQL Schema

  • Define your field resolvers separately from the schema. Since the schema already describes all of the fields, arguments, and result types, the only thing left is a collection of functions that are called to actually execute these fields.
  • Keep in mind that GraphQL resolvers can return promises.
    • In fact, most resolvers that do real work - for example fetching data from a database or a REST API - will return a promise. If you’re not familiar with promises, here’s a brief overview.
1type Query {
2 numberSix: Int! # Should always return the number 6 when queried
3 numberSeven: Int! # Should always return 7
4}

Exercise 5 - Implementing temporary resolvers for Queries

  • resolvers/postsResolvers.js - will export resolvers relevant to posts

  • resolvers/authorsResolvers.js - will export resolvers relevant to authors

  • You define all of your server’s resolvers in a single JavaScript object (named resolvers above).

    • This object is called the resolver map.
    • The resolver map has top-level fields that correspond to your schema’s types (such as Query above).
  • Each resolver function belongs to whichever type its corresponding field belongs to.

1query getPosts {
2 posts(order: DESC, limit: 3) {
3 id
4 title
5 author {
6 name
7 avatarUrl
8 }
9 }
10}
1{
2 "data": {
3 "posts": [
4 {
5 "id": "12",
6 "title": "Reprehenderit excepteur quis nulla dolore elit est velit laboris et adipisicing Lorem adipisicing labore.",
7 "author": {
8 "name": "Name",
9 "avatarUrl": "https://images.unsplash.com/photo-1510227272981-87123e259b17?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=3759e09a5b9fbe53088b23c615b6312e"
10 }
11 },
12 {
13 "id": "11",
14 "title": "Laboris nulla pariatur incididunt velit voluptate ea.",
15 "author": {
16 "name": "Name",
17 "avatarUrl": "https://images.unsplash.com/photo-1513732822839-24f03a92f633?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjE3Nzg0fQ"
18 }
19 },
20 {
21 "id": "10",
22 "title": "Ea do irure aliqua mollit amet ex proident.",
23 "author": {
24 "name": "Name",
25 "avatarUrl": "https://images.unsplash.com/photo-1510227272981-87123e259b17?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=3759e09a5b9fbe53088b23c615b6312e"
26 }
27 }
28 ]
29 }
30}

Exercise 6 - Batch REST requests with REST Data Source

  • explore data/postsDataSource.js implement getAuthorById method
1class PostsJsonDataSource extends DataSource {
2 constructor() {
3 super()
4 // Creating InMemoryCache
5 this.keyValueCache = new InMemoryLRUCache()
6 this.jsonDbPath = path.resolve(__dirname, "../db.json")
7 }
8
9 // Caching our json file and limiting access to file
10 async get(key) {
11 console.log(`File access for ${key}`)
12 const result = await readFile(this.jsonDbPath)
13 const parsedResult = JSON.parse(result)
14 return parsedResult[key]
15 }
16
17 async getPosts() {
18 const posts = await this.get(`posts`)
19 return posts
20 }
21
22 async getAuthorById(id) {
23 const authors = await this.get(`authors`)
24 return authors.filter((author) => author.id === id)[0]
25 }
26}

Extra Credit

Resolve it:

1const cache = await this.keyValueCache.get(CACHE_KEY)
2if (!cache) {
3 console.log(`File access for${key}`)
4 const result = await readFile(this.jsonDbPath)
5 const parsedResult = JSON.parse(result)
6 await this.keyValueCache.set(CACHE_KEY, parsedResult)
7 return parsedResult[key]
8}
9return cache[key]

Exercise Exercise 7 - Migrate to the same data source

1class PostsJsonDataSource extends DataSource {
2 constructor() {
3 super()
4 // Creating InMemoryCache
5 this.keyValueCache = new InMemoryLRUCache()
6 this.jsonDbPath = path.resolve(__dirname, "../db.json")
7 }
8
9 // Caching our json file and limiting access to file
10 async get(key) {
11 // ----- Extra Credit ----
12 // const cache = await this.keyValueCache.get(CACHE_KEY)
13 // if (!cache) {
14 // console.log(`File access for${key}`)
15 // const result = await readFile(this.jsonDbPath)
16 // const parsedResult = JSON.parse(result)
17 // await this.keyValueCache.set(CACHE_KEY, parsedResult)
18 // return parsedResult[key]
19 // }
20 // return cache[key]
21 console.log(`File access for ${key}`)
22 const result = await readFile(this.jsonDbPath)
23 const parsedResult = JSON.parse(result)
24 return parsedResult[key]
25 }
26
27 async getPosts() {
28 const posts = await this.get(`posts`)
29 return posts
30 }
31
32 async getAuthorById(id) {
33 const authors = await this.get(`authors`)
34 return authors.filter((author) => author.id === id)[0]
35 }
36}

© Lauro Silva, LLC. All rights reserved.