GraphQL w React #1 – poznaj podstawy GraphQL!

Spis treści

  1. Wprowadzenie
  2. Podstawy GraphQL
    1. Schema
      1. Scalar types
      2. Object types
      3. Input types
      4. Root types
    2. Resolvers
  3. Uruchomienie serwera GraphQL
    1. Instalacja i implementacja
      1. Instalacja serwera Apollo
      2. Uruchomienie serwera Apollo
    2. Pełny kod aplikacji
    3. Kod w repozytorium Github
    4. Queries – wysyłanie żądań na serwer
      1. Kod w repozytorium Github
  4. Podsumowanie

Wprowadzenie

Popularność GraphQL rośnie z dnia na dzień – warto więc być zorientowanym w temacie, zważywszy że jego stosowanie ma wiele zalet:

  • jasno zdefiniowane API dzięki schematom
  • pobieranie tylko potrzebnych danych – brak zjawiska nadmiarowych danych
  • Brak konieczności poprawek backendowych, jeżeli zmienią się potrzebne w danym widoku dane (w rozsądnych ramach oczywiście)
  • duża popularność, GraphQL jest stosowany między innymi przez takie firmy Github, Starbucks czy Twitter.

Warto więc poznać podstawy GraphQL.


Podstawy GraphQL

GraphQL to język zapytań do API, w którym po stronie klienta podajesz jakie dokładnie dane chcesz dostać a strona serwera odpowiada za przesłanie dokładnie tylko tych danych. Omówimy teraz podstawowe zagadnienia związane z GrapghQL

Schema

Schema jest definicją danych jakie będą używane przez graf. Definiujemy typy danych jak również żądania jakie będzie wspierał nasz serwer.

Istnieją 3 typy składników z których tworzymy schemat:

  1. Scalar types
  2. Object types
  3. Input types
  4. Root types

Scalar types

Są to typy proste, do których my sami nie musimy pisać resolverów (o resolverach będzie nieco później). Na typy proste składają się:

  • Int – 32 bitowa liczba całkowita
  • Float – liczba zmiennoprzecinkowa
  • String – wartość tekstowa
  • Boolean – wartość true lub false
  • ID – wartość traktowana jak String, z założenia jednak będąca unikalna dla obiektu

Object Types

Typy złożone z typów prostych, jak i innych typów złożonych. Przykładowo typ:

type Animal {
  id: ID!
  name: String!
  birthPlace: String
  nicknames: [String]
  owner: Owner!
}

type Owner {
  id: Int!
  name: String!
}

Oznacza obiekt posiadający obowiązkowe parametry id typu ID, name typu String , a także nieobowiązkowe birthPlace typu String oraz nicknames – listę elementów typu String. Istnieje również pole typu Owner – gdzie Owner to również obiekt.
To wykrzyknik określa, że dane pole jest wymagane – gdy go nie ma, wartość pola może być nullem.

Input types

Zasadniczo Input types są bardzo podobne do Object types, z tym że Input types to obiekty używane w zapytaniach do obsługi żądań zapisu danych. Stwórzmy obiekt który będziemy wysyłać przy tworzeniu nowego obiektu typu Owner:

input OwnerInput {
   name: String!
 }

Root types

Root types określają typy specjalne, które pozwalają na obsługę żądań serwera. Istnieją 3 typy:

  1. Query, które obsługuje żądania odczytu danych
  2. Mutation, które obsługuje żądania zapisu danych
  3. Subscription, które zestawia stałe połączenie z serwerem

Ponieważ Subscription wymaga stałego dwukierunkowego połączenia, przez np. WebSocket, nie będzie tutaj poruszony. Typ Query jest wymagany. Spójrzmy na przykład:

type Query {
   animals: [Animal]
   owners: [Owner]
   animal(name: String!): Animal
   owner(id: Int!): Owner
 }
  
 type Mutation {
   createOwner(owner: OwnerInput!): Owner!
 }

Zdefiniowaliśmy więc możliwość pobrania danych dla listy zwierząt, pojedynczego zwierzęcia oraz pojedynczego właściciela.
Dodatkowo zdefiniowaliśmy możliwość dodania nowego właściciela.

Resolvers

Resolvery odpowiadają za sposób, w jaki rozwiązywane są dane potrzebne do zapytań określonych w Root Types Query . Jako źródło danych posłużą nam zwykłe tablice:

const owners = [
  {
    id: 1, 
    name: 'Radek 1',
  },
  {
    id: 2, 
    name: 'Radek 2',
  },
]

const animals = [
  {
    id: 1,
    name: 'Burek', 
    birthPlace: 'Cracow', 
    ownerId: 1,
    nicknames: ['Bury'],
  },
  {
    id: 2,
    name: 'Azor',
    birthPlace: 'Warsaw',
    ownerId: 1,
    nicknames: ['Azorek', 'Maly'],
  },
  {
    id: 1,
    name: 'Reksio',
    birthPlace: 'Cracow',
    ownerId: 2,
    nicknames: [],
  },
];

Zadeklarujmy teraz obiekt zawierający resolvery – czyli funkcje odpowiedzialne za zwracanie wartości pól opisanych w podrozdziale Root Types, a także pól niestandardowych na obiektach:

const resolvers = {
  Query: {
    animals: () => animals,
    owners: () => owners,
    animal: (parent, args, context, info) => animals
      .find(animal => animal.name === args.name),
    owner: (parent, args, context, info) => owners
      .find(el => el.id === args.id)
  },
  Animal: {
    owner: (parent, args, context, info) => owners
      .find(el => el.id === parent.ownerId)
  },
  Mutation: {
    createOwner: (parent, args, context, info) => {
      let owner = {...args.owner, id: owners.length + 1};
      owners.push(owner);
      return owner;
    }
  }
}

W naszym przypadku takim polem niestandardowym jest pole owner w typie Animal. Ponieważ typ Animal , będący rodzicem zagnieżdżonego w nim typu Owner, zawiera pole ownerId, możemy po nim przeszukać tabelę właścicieli.

W tym momencie mamy wszystko, by móc uruchomić serwer GraphQL.


Uruchomienie serwera GraphQL

Instalacja i implementacja

Instalacja serwera Apollo

Ponieważ skorzystamy z serwera platformy Apollo, musimy ją zainstalować. Dodatkowo musimy zainstalować samego GraphQL oraz narzędzie nodemon, które będzie restartować serwer Node za każdym razem gdy zapiszemy zmiany

npm install apollo-server graphql nodemon

Uruchomienie serwera Apollo

Zaimportujmy klasę serwera oraz funkcję, która pozwoli przeparsować tekst na deklarację typów GraphQL:

const { ApolloServer, gql } = require("apollo-server");

Samo uruchomienie serwera wygląda następująco:

const server = new ApolloServer({
  typeDefs: types,
  resolvers,
})

server.listen()
  .then(({url}) => console.log(`Server url: ${url}`))

Pełny kod aplikacji

Poniżej znajduje się pełny kod aplikacji, gotowy do uruchomienia po zainstalowaniu zależności:

const { ApolloServer, gql } = require("apollo-server");

const types = gql`
  type Animal {
    id: ID!
    name: String!
    birthPlace: String
    nicknames: [String]
    owner: Owner!
  }
  
  type Owner {
    id: Int!
    name: String!
  }

  input OwnerInput {
    name: String!
  }
  
  type Query {
    animals: [Animal]
    owners: [Owner]
    animal(name: String!): Animal
    owner(id: Int!): Owner
  }
  
  type Mutation {
    createOwner(owner: OwnerInput!): Owner!
  }
`

const owners = [
  {
    id: 1,
    name: 'Radek 1',
  },
  {
    id: 2,
    name: 'Radek 2',
  },
]

const animals = [
  {
    id: 1,
    name: 'Burek',
    birthPlace: 'Cracow',
    ownerId: 1,
    nicknames: ['Bury'],
  },
  {
    id: 2,
    name: 'Azor',
    birthPlace: 'Warsaw',
    ownerId: 1,
    nicknames: ['Azorek', 'Maly'],
  },
  {
    id: 1,
    name: 'Reksio',
    birthPlace: 'Cracow',
    ownerId: 2,
    nicknames: [],
  },
];

const resolvers = {
  Query: {
    animals: () => animals,
    owners: () => owners,
    animal: (parent, args, context, info) => animals
      .find(animal => animal.name === args.name),
    owner: (parent, args, context, info) => owners
      .find(el => el.id === args.id)
  },
  Animal: {
    owner: (parent, args, context, info) => owners
      .find(el => el.id === parent.ownerId)
  },
  Mutation: {
    createOwner: (parent, args, context, info) => {
      let owner = {...args.owner, id: owners.length + 1};
      owners.push(owner);
      return owner;
    }
  }
}

const server = new ApolloServer({
  typeDefs: types,
  resolvers,
})

server.listen()
  .then(({url}) => console.log(`Server url: ${url}`))

Kod w repozytorium Github

Pełny kod aplikacji jest dostępny pod adresem
https://github.com/radek-anuszewski/graphql-example

Queries – wysyłanie żądań na serwer

Po utworzeniu serwera i uruchomieniu komendy

npm start

Uruchomi się playground, który umożliwi wysyłanie żądań na utworzony przez nas serwer.

Dodajmy nowego właściciela poprzez uruchomienie:

mutation {
  createOwner(owner: {name: "R"}) {
    id,
    name,
  }
}

W odpowiedzi od serwera dostaniemy utworzony właśnie obiekt. Wyślijmy zapytanie które pobierze listę właścicieli by sprawdzić, czy utworzony element się tam znajduje:

query {
  owners {
    name
  }
}

W tym wypadku nie pobieramy parametru id.

Kod w repozytorium Github

Więcej przykładów żądań jakie możesz wysłać na serwer znajdziesz w pliku queries.txt:

https://github.com/radek-anuszewski/graphql-example/blob/master/queries.txt


Podsumowanie

GraphQL to narzędzie, którego popularność coraz bardziej rośnie. Zapewniana przez nie elastyczność powoduje, że coraz więcej firm chce je stosować. W kolejnym poście dowiesz się, w jaki sposób połączyć GraqhQL z React.