Spis treści
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:
- Scalar types
- Object types
- Input types
- 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:
- Query, które obsługuje żądania odczytu danych
- Mutation, które obsługuje żądania zapisu danych
- 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.