GraphQL
REST war gestern
Jens Siebert (@jens_siebert)
WebMontag Kassel, 10. Dezember 2018
https://www.slideshare.net/JensSiebert1
REST ist super!
aber es hat so seine Probleme…
Probleme mit REST: Overfetching
https://swapi.co/api/people/1/
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.co/api/planets/1/",
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
],
"species": [
"https://swapi.co/api/species/1/"
],
"vehicles": [
"https://swapi.co/api/vehicles/14/",
"https://swapi.co/api/vehicles/30/“
],
"starships": [
"https://swapi.co/api/starships/12/",
"https://swapi.co/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.co/api/people/1/"
}
Probleme mit REST: Underfetching
https://swapi.co/api/people/1/
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.co/api/planets/1/",
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
], …
https://swapi.co/api/films/3/
{
"title": "Return of the Jedi",
"episode_id": 6,
"opening_crawl": "Luke Skywalker has...",
"director": "Richard Marquand",
"producer": "Howard G. Kazanjian, George Lucas",
"release_date": "1983-05-25",
"characters": [
"https://swapi.co/api/people/1/",
"https://swapi.co/api/people/2/",
"https://swapi.co/api/people/3/",
"https://swapi.co/api/people/4/",
"https://swapi.co/api/people/5/",
"https://swapi.co/api/people/10/",
"https://swapi.co/api/people/13/",
…
Probleme mit REST: Endpoint Management
https://swapi.co/api/people
https://swapi.co/api/films
https://swapi.co/api/planets
https://swapi.co/api/starships
https://swapi.co/api/vehicles
https://swapi.co/api/characters-with-movie-title-and-director 🤢
GraphQL
• Kein Framework!
• Sprachspezifikation
• Deklarative Abfragesprache
• Deklarative Schema-Beschreibungssprache
• Beschreibt einen „Informationsgraph“ und die Abfragen darauf
• Implementierungen in verschiedenen Sprachen/Frameworks verfügbar
Lösung mit GraphQL: Overfetching
https://graphql.org/swapi-graphql
query {
person(personID: 1) {
name
height
gender
}
}
{
"data": {
"person": {
"name": "Luke Skywalker",
"height": 172,
"gender": "male"
}
}
}
Lösung mit GraphQL: Underfetching
https://graphql.org/swapi-graphql
query {
person(personID: 1) {
name
height
gender
filmConnection {
films {
title
director
}
}
}
}
{
"data": {
"person": {
"name": "Luke Skywalker",
"height": 172,
"gender": "male",
"filmConnection": {
"films": [
{
"title": "A New Hope",
"director": "George Lucas"
},…
Lösung mit GraphQL: Endpoint Management
https://graphql.org/swapi-graphql
Sprachelemente: Query
http://snowtooth.moonhighway.com
query {
allLifts {
id
name
status
}
}
{
"data": {
"allLifts": [
{
"id": "astra-express",
"name": "Astra Express",
"status": "OPEN"
},…
Sprachelemente: Query
http://snowtooth.moonhighway.com
query {
allLifts {
id
name
status
trailAccess {
name
status
}
}
}
{
"data": {
"allLifts": [
{
"id": "astra-express",
"name": "Astra Express",
"status": "OPEN",
"trailAccess": [
{
"name": "Blue Bird",
"status": "OPEN"
},…
Kante im Graph
Sprachelemente: Mutation
http://snowtooth.moonhighway.com
mutation {
setLiftStatus(id: "astra-express", status: CLOSED) {
id
name
status
}
}
{
"data": {
"setLiftStatus": {
"id": "astra-express",
"name": "Astra Express",
"status": "CLOSED"
}
}
}
Sprachelemente: Subscription
http://snowtooth.moonhighway.com
subscription {
liftStatusChange {
id
name
status
}
}
mutation {
setLiftStatus(id: "astra-express", status: CLOSED) {
id
name
status
}
}
{
"data": {
"liftStatusChange": {
"id": "astra-express",
"name": "Astra Express",
"status": “CLOSED"
}
}
}
OPEN -> CLOSED
Sprachelemente: Introspection
http://snowtooth.moonhighway.com
query {
__schema {
types {
name
kind
description
}
}
}
{
"data": {
"__schema": {
"types": [
{
"name": "Query",
"kind": "OBJECT",
"description": ""
},
{
"name": "LiftStatus",
"kind": "ENUM",
"description": "An enum describing …"
},
Sprachelemente: Typ-Definitionen
scalar DateTime
type Photo {
id: ID!
name: String!
url: String!
description: String
category: PhotoCategory!
postedBy: User!
taggedUsers: [User!]!
created: DateTime!
}
Basisdatentypen:
• ID
• Int
• Float
• String
• Boolean
Nullable Type
Non-Nullable Type
Custom Type
Non-Nullable List of Non-Nullable Type
Sprachelemente: Query/Mutations-Definition
type Query {
me: User
totalPhotos: Int!
allPhotos: [Photo!]!
Photo(id: ID!): Photo
totalUsers: Int!
allUsers: [User!]!
User(login: ID!): User
}
type Mutation {
postPhoto(input: PostPhotoInput!): Photo!
tagPhoto(githubLogin:ID! photoID:ID!): Photo!
…
}
Analog: Subscriptions
Implementierung: Type-Resolver
module.exports = {
Photo: {
id: parent => parent.id || parent._id,
url: parent => `/img/photos/${parent._id}.jpg`,
postedBy: (parent, args, { db }) => db.collection('users').findOne({ githubLogin: parent.userID }),
taggedUsers: async (parent, args, { db }) => {
const tags = await db.collection('tags').find().toArray()
const logins = tags
.filter(t => t.photoID === parent._id.toString())
.map(t => t.githubLogin)
return db.collection('users').find({ githubLogin: { $in: logins }}).toArray()
}
}, …
}
Implementierung: Query-Resolver
module.exports = {
me: (parent, args, { currentUser }) => currentUser,
totalPhotos: (parent, args, { db }) => db.collection('photos').estimatedDocumentCount(),
allPhotos: (parent, args, { db }) => db.collection('photos').find().toArray(),
Photo: (parent, args, { db }) => db.collection('photos').findOne({ _id: ObjectID(args.id) }),
totalUsers: (parent, args, { db }) => db.collection('users').estimatedDocumentCount(),
allUsers: (parent, args, { db }) => db.collection('users').find().toArray(),
User: (parent, args, { db }) => db.collection('users').findOne({ githubLogin: args.login })
}
Implementierung: Mutation-Resolver
async postPhoto(parent, args, { db, currentUser }) {
if (!currentUser) {
throw new Error('only an authorized user can post a photo').
}
const newPhoto = {
...args.input,
userID: currentUser.githubLogin,
created: new Date()
}
const { insertedIds } = await db.collection('photos').insert(newPhoto)
newPhoto.id = insertedIds[0]
return newPhoto
}
Implementierung: Server mit Apollo GraphQL
const resolvers = require('./resolvers')
const typeDefs = readFileSync('./typeDefs.graphql', 'UTF-8')
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
…
}
})
Sicherheit
• Authorization
• Request Timeouts
• Data Limitation (Paging)
• Query Depth Limitation
• Query Complexity Limitation
• Monitoring
const depthLimit =
require('graphql-depth-limit')
const { createComplexityLimitRule } =
require('graphql-validation-complexity')
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5),
createComplexityLimitRule(1000,
onCost: cost => console.log('query cost: ', cost))
],
context: async({ req, connection }) = > { ... }
})
Literatur
Vielen Dank!
https://graphql.org
https://graphql.org/learn
https://www.apollographql.com
Slides: https://www.slideshare.net/JensSiebert1
Code: https://github.com/moonhighway/learning-graphql
Beispiel: http://snowtooth.moonhighway.com
Twitter: @jens_siebert

GraphQL

  • 1.
    GraphQL REST war gestern JensSiebert (@jens_siebert) WebMontag Kassel, 10. Dezember 2018 https://www.slideshare.net/JensSiebert1
  • 2.
    REST ist super! aberes hat so seine Probleme…
  • 3.
    Probleme mit REST:Overfetching https://swapi.co/api/people/1/ { "name": "Luke Skywalker", "height": "172", "mass": "77", "hair_color": "blond", "skin_color": "fair", "eye_color": "blue", "birth_year": "19BBY", "gender": "male", "homeworld": "https://swapi.co/api/planets/1/", "films": [ "https://swapi.co/api/films/2/", "https://swapi.co/api/films/6/", "https://swapi.co/api/films/3/", "https://swapi.co/api/films/1/", "https://swapi.co/api/films/7/" ], "species": [ "https://swapi.co/api/species/1/" ], "vehicles": [ "https://swapi.co/api/vehicles/14/", "https://swapi.co/api/vehicles/30/“ ], "starships": [ "https://swapi.co/api/starships/12/", "https://swapi.co/api/starships/22/" ], "created": "2014-12-09T13:50:51.644000Z", "edited": "2014-12-20T21:17:56.891000Z", "url": "https://swapi.co/api/people/1/" }
  • 4.
    Probleme mit REST:Underfetching https://swapi.co/api/people/1/ { "name": "Luke Skywalker", "height": "172", "mass": "77", "hair_color": "blond", "skin_color": "fair", "eye_color": "blue", "birth_year": "19BBY", "gender": "male", "homeworld": "https://swapi.co/api/planets/1/", "films": [ "https://swapi.co/api/films/2/", "https://swapi.co/api/films/6/", "https://swapi.co/api/films/3/", "https://swapi.co/api/films/1/", "https://swapi.co/api/films/7/" ], … https://swapi.co/api/films/3/ { "title": "Return of the Jedi", "episode_id": 6, "opening_crawl": "Luke Skywalker has...", "director": "Richard Marquand", "producer": "Howard G. Kazanjian, George Lucas", "release_date": "1983-05-25", "characters": [ "https://swapi.co/api/people/1/", "https://swapi.co/api/people/2/", "https://swapi.co/api/people/3/", "https://swapi.co/api/people/4/", "https://swapi.co/api/people/5/", "https://swapi.co/api/people/10/", "https://swapi.co/api/people/13/", …
  • 5.
    Probleme mit REST:Endpoint Management https://swapi.co/api/people https://swapi.co/api/films https://swapi.co/api/planets https://swapi.co/api/starships https://swapi.co/api/vehicles https://swapi.co/api/characters-with-movie-title-and-director 🤢
  • 6.
    GraphQL • Kein Framework! •Sprachspezifikation • Deklarative Abfragesprache • Deklarative Schema-Beschreibungssprache • Beschreibt einen „Informationsgraph“ und die Abfragen darauf • Implementierungen in verschiedenen Sprachen/Frameworks verfügbar
  • 7.
    Lösung mit GraphQL:Overfetching https://graphql.org/swapi-graphql query { person(personID: 1) { name height gender } } { "data": { "person": { "name": "Luke Skywalker", "height": 172, "gender": "male" } } }
  • 8.
    Lösung mit GraphQL:Underfetching https://graphql.org/swapi-graphql query { person(personID: 1) { name height gender filmConnection { films { title director } } } } { "data": { "person": { "name": "Luke Skywalker", "height": 172, "gender": "male", "filmConnection": { "films": [ { "title": "A New Hope", "director": "George Lucas" },…
  • 9.
    Lösung mit GraphQL:Endpoint Management https://graphql.org/swapi-graphql
  • 10.
    Sprachelemente: Query http://snowtooth.moonhighway.com query { allLifts{ id name status } } { "data": { "allLifts": [ { "id": "astra-express", "name": "Astra Express", "status": "OPEN" },…
  • 11.
    Sprachelemente: Query http://snowtooth.moonhighway.com query { allLifts{ id name status trailAccess { name status } } } { "data": { "allLifts": [ { "id": "astra-express", "name": "Astra Express", "status": "OPEN", "trailAccess": [ { "name": "Blue Bird", "status": "OPEN" },… Kante im Graph
  • 12.
    Sprachelemente: Mutation http://snowtooth.moonhighway.com mutation { setLiftStatus(id:"astra-express", status: CLOSED) { id name status } } { "data": { "setLiftStatus": { "id": "astra-express", "name": "Astra Express", "status": "CLOSED" } } }
  • 13.
    Sprachelemente: Subscription http://snowtooth.moonhighway.com subscription { liftStatusChange{ id name status } } mutation { setLiftStatus(id: "astra-express", status: CLOSED) { id name status } } { "data": { "liftStatusChange": { "id": "astra-express", "name": "Astra Express", "status": “CLOSED" } } } OPEN -> CLOSED
  • 14.
    Sprachelemente: Introspection http://snowtooth.moonhighway.com query { __schema{ types { name kind description } } } { "data": { "__schema": { "types": [ { "name": "Query", "kind": "OBJECT", "description": "" }, { "name": "LiftStatus", "kind": "ENUM", "description": "An enum describing …" },
  • 15.
    Sprachelemente: Typ-Definitionen scalar DateTime typePhoto { id: ID! name: String! url: String! description: String category: PhotoCategory! postedBy: User! taggedUsers: [User!]! created: DateTime! } Basisdatentypen: • ID • Int • Float • String • Boolean Nullable Type Non-Nullable Type Custom Type Non-Nullable List of Non-Nullable Type
  • 16.
    Sprachelemente: Query/Mutations-Definition type Query{ me: User totalPhotos: Int! allPhotos: [Photo!]! Photo(id: ID!): Photo totalUsers: Int! allUsers: [User!]! User(login: ID!): User } type Mutation { postPhoto(input: PostPhotoInput!): Photo! tagPhoto(githubLogin:ID! photoID:ID!): Photo! … } Analog: Subscriptions
  • 17.
    Implementierung: Type-Resolver module.exports ={ Photo: { id: parent => parent.id || parent._id, url: parent => `/img/photos/${parent._id}.jpg`, postedBy: (parent, args, { db }) => db.collection('users').findOne({ githubLogin: parent.userID }), taggedUsers: async (parent, args, { db }) => { const tags = await db.collection('tags').find().toArray() const logins = tags .filter(t => t.photoID === parent._id.toString()) .map(t => t.githubLogin) return db.collection('users').find({ githubLogin: { $in: logins }}).toArray() } }, … }
  • 18.
    Implementierung: Query-Resolver module.exports ={ me: (parent, args, { currentUser }) => currentUser, totalPhotos: (parent, args, { db }) => db.collection('photos').estimatedDocumentCount(), allPhotos: (parent, args, { db }) => db.collection('photos').find().toArray(), Photo: (parent, args, { db }) => db.collection('photos').findOne({ _id: ObjectID(args.id) }), totalUsers: (parent, args, { db }) => db.collection('users').estimatedDocumentCount(), allUsers: (parent, args, { db }) => db.collection('users').find().toArray(), User: (parent, args, { db }) => db.collection('users').findOne({ githubLogin: args.login }) }
  • 19.
    Implementierung: Mutation-Resolver async postPhoto(parent,args, { db, currentUser }) { if (!currentUser) { throw new Error('only an authorized user can post a photo'). } const newPhoto = { ...args.input, userID: currentUser.githubLogin, created: new Date() } const { insertedIds } = await db.collection('photos').insert(newPhoto) newPhoto.id = insertedIds[0] return newPhoto }
  • 20.
    Implementierung: Server mitApollo GraphQL const resolvers = require('./resolvers') const typeDefs = readFileSync('./typeDefs.graphql', 'UTF-8') const server = new ApolloServer({ typeDefs, resolvers, context: async ({ req }) => { … } })
  • 21.
    Sicherheit • Authorization • RequestTimeouts • Data Limitation (Paging) • Query Depth Limitation • Query Complexity Limitation • Monitoring const depthLimit = require('graphql-depth-limit') const { createComplexityLimitRule } = require('graphql-validation-complexity') const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ depthLimit(5), createComplexityLimitRule(1000, onCost: cost => console.log('query cost: ', cost)) ], context: async({ req, connection }) = > { ... } })
  • 22.
  • 23.
    Vielen Dank! https://graphql.org https://graphql.org/learn https://www.apollographql.com Slides: https://www.slideshare.net/JensSiebert1 Code:https://github.com/moonhighway/learning-graphql Beispiel: http://snowtooth.moonhighway.com Twitter: @jens_siebert

Editor's Notes

  • #7 Start: 2012 Initiale Version der Spezifikation und Referenzimplementierung: 2015 Production-Ready: 2016
  • #18 Parent => Eltern-Objekt => in diesem Fall ein Typ-Objekt