Introduzione a GraphQL

Cosa abbiamo imparato da REST

Per molti sviluppatori, costruire un API per le loro applicazioni significa essenzialmente mappare le risorse del dominio a degli URI, seguendo i principi dettati da REST. Di solito creare un sistema RESTful non è complicato, e l’idea di fondo è abbastanza semplice da renderci la vita facile. Vediamo alcune delle conseguenze e benefici che nascono dall’utilizzo di REST:

  • Ogni verbo HTTP ha un significato preciso, permettendo allo sviluppatore di capire immediatamente che tipo di operazione (fra le classiche CRUD) verra eseguità sulla risorsa.
  • Spesso, la stessa URI può essere usata con molteplici verbi per compiere operazioni differenti (per esempio, “example.com/tag/123” fa riferimento ad uno specifico tag che possiamo fetchare, aggiornare o cancellare).
  • In molti frameworks server-side, i controllers possono essere usati con un approccio RESTful, in modo che ognuno rappresenti una o più risorse.

 

Purtroppo, ci si trova spesso ad affrontare anche i seguenti problemi:

  • URIs complessi possono essere difficili da scrivere e comprendere, portando ad interpretazioni arbitrarie.
  • Per filtrare delle collections potrebbe essere necessario inserire degli ids nella URL, ma anche query parameters (per esempio per filtrare su campi specifici).
  • Il client non ha a disposizione un metodo standard per specificare di quali proprietà della risorsa necessita, ed il server semplicemente restituisce tutti i dati che ha a disposizione.
  • Forse la cosa più importante è che il client non ha modo di chiedere la restituzione di risorse correlate a quella a cui si fa riferimento nella URI, se non con stratagemmi non standard e arbitrari decisi da chi scrive l’API.

REST ha poche colpe riguardo i problemi appena citati. Ciò che lo ha reso così diffuso è stata proprio la semplicità, molto benvenuta dalla maggior parte degli sviluppatori. Il transport layer de facto è HTTP (anche se tecnicamente non l’unico utilizzabile), che porta con sè limiti e regole specifiche.

Vediamo ora un altro approccio per la definizione e costruzione di API, che possibilmente tenti di risolvere i problemi di cui sopra.

Un nuovo modo di sviluppare API: GraphQL

GraphQL mantiene gli stessi benefici dell’utilizzo di REST (si affida principalmente ad HTTP, ha pochissimo overhead, è stateless e facilmente cacheable) ma apporta utili migliorie. Viene creato in Facebook nel 2012, dalla necessità di fornire ai molteplici clients (soprattutto sito mobile e app) un modo per richiedere esattamente i dati di cui avevano bisogno, riducendo il peso delle risposte e minimizzando il traffico totale. In questo post non verranno descritte tutte le features, per quello potete fare riferimento alla guida ufficiale, semplicemente vedremo cosa rende GraphQL interessante. Guarderemo ora un esempio, per il quale trovate il codice completo in questo repository.

 

Query con tipi e fields

Esempio #1

query {
  games {
        id
      title
  }
}

 

Lo snippet precedente definisce una query fatta dal client quando necessita soltanto dell’id ed il titolo di un gioco. Dal momento che nessun argomento è stato passato per filtrare i dati dello specifico tipo che stiamo richiedendo (game), allora tutti gli elementi verranno restituiti. Quello che ci viene restituito dal server è il seguente:

{
  "data": {
    "games": [{
        "id": 1,
        "title": "Frogger"
      }, {
        "id": 2,
        "title": "Galaxian"
      }, {
        "id": 3,
        "title": "Tiger Road"
      }, {
        "id": 4,
        "title": "Mendel Palace"
      }
    ]
  }
}

 

Con REST, otterremmo lo stesso risultato con la seguente URL:

GET /game?fields=id,title

 

Da notare che con un server REST probabilmente non avremmo la possibilità di definire a quali campi siamo interessati.

 

Example #2

query {
  games(id:2) {
        id
        title
        year
  }
}

 

Example #3

 

query {
  games(title:"Frogger") {
    id
        title
        year
  }
}

 

Gli esempi #2 e #3 mostrano come sia possibile filtrare anche per id e title. L’id non ha nessun significato semantico (al contrario di REST) ed ogni filtro può essere rappresentato seguendo lo stesso pattern chiave/valore (fieldname: “value”).

L’equivalente REST per l’esempio #2:

GET /game/2

 

E #3:

GET /game?title=Frogger

 

 

Relazioni fra Entità: il grafo

 

Example #4

query {
    games(id:2) {
        title
        developer {
            name
            nation
        }
    }
}

 

 

I tipi chiamati Game e Company sono in relazione; nel nostro esempio, per semplicità, un gioco può avere una sola azienda come developer ed una come publisher, mentre qualsiasi azienda può ovviamente avere molteplici giochi sviluppati e pubblicati. Nell’esempio #4, developer è di tipo Company: questo ci permette di richiedere campi specifici di quel tipo, come ad esempio name e location. Gli stessi tipi possono essere usati al contrario, per ottenere tutti i giochi di una specifica azienda:

 

Example #5

query {
    companies(name:"Game Freak") {
        name
        gamesAsDeveloper {
            title
        }
    }
}

 

Vediamo come si possono descrivere queste relazioni in Javascript. La definizione del tipo Game:

developer: {
  type: Company,
    description: 'The developer of the game',
}

 

Ed il tipo Company:

gamesAsDeveloper: {
    type: new GraphQLList(Game),
    description: 'The games created as a developer',
}

 

Il field developer è di tipo Company, mentre il campo gameAsDeveloper è una lista di oggetti di tipo Game. Fate riferimento al repository per vedere l’implementazione completa.

 

Minor numero possibile di richieste al server

 

Questa volta ci serve una lista di aziende Giapponesi e alcune informazioni rispetto ai loro giochi, ma soltanto per la piattaforma NES.

 

Esempio #6

query {
    companies(nation:"Japan") {
      name
        gamesAsDeveloper(platform:"NES") {
        title
          year
        }
        gamesAsPublisher(platform:"NES") {
            title
            year
        }
    }
}

 

L’output di questa query:

{
  "data": {
    "companies": [{
        "name": "Konami",
        "gamesAsDeveloper": [],
        "gamesAsPublisher": []
      }, {
        "name": "Namco",
        "gamesAsDeveloper": [],
        "gamesAsPublisher": [{
            "title": "Mendel Palace",
            "year": "1989"
          }]
      }, {
        "name": "Game Freak",
        "gamesAsDeveloper": [{
            "title": "Mendel Palace",
            "year": "1989"
          }],
        "gamesAsPublisher": []
      }, {
        "name": "Capcom",
        "gamesAsDeveloper": [{
            "title": "Tiger Road",
            "year": "1987"
          }],
        "gamesAsPublisher": []
      }
    ]
  }
}

 

In un api REST, avremmo potuto ottenere lo stesso risultato con una di queste modalità:

  • Facendo una richiesta ad un endpoint custom che accettasse come filtri aziende e giochi
    GET /companyWithGames?companyNation=Japan&gamePlatform=NES&fields=...

     

  • Facendo molteplici richieste a diversi endpoint generici
    GET /company?nation=Japan&fields=...
    GET /game?developerName=[developer 1 name]&gamePlatform=NES&fields=...
    GET /game?developerName=[developer 2 name]&gamePlatform=NES&fields=...
    GET ...

     

 

Alcune cose da notare:

  • L’espressività del query language diventa evidente quando cominciamo a scrivere query più complesse. Il filtraggio dei dati viene naturale ed adotta una sintassi uniforme.
  • Lato server, ogni funzione incaricata di restituire porzioni del risultato finale possono essere eseguite in parallelo (quando possibile) ed ogni funzione ha conoscenza dei dati restituiti dal livello superiore.

 

Documentazione dell’API

 

Quando progettiamo un webservice REST, abbiamo molta libertà nello stabilire il formato della richiesta e della risposta. Sfortunatamente non esiste nessun modo per documentare in maniera uniforme queste informazioni, in modo che possano essere consumate da un client.

Con GraphQL, usando l’introspezione, è possibile richiedere al server informazioni relative ai tipi ed i loro campi.

 

Esempio #7:

query {
    __schema {
        types {
            kind
            name
            description
        }
    }
}

 

Una query con il query root __schema restituisce la lista dei tipi con le loro descrizioni e fields figli:

{
  "data": {
    "__schema": {
      "types": [
    ...
        {
          "kind": "OBJECT",
          "name": "Company",
          "description": "A company object",
          "fields": [...]
        }, {
          "kind": "OBJECT",
          "name": "Game",
          "description": "A game object"
          "fields": [...]
        },
        ...
}

 

Un altro modo di ottenere informazioni riguardo i tipi è di sfruttare il query root __type, passando il nome di un tipo specifico:

 

Esempio #8

query {
    __type(name:"Game") {
        fields {
            name
            description
            type {
                name
            }
        }
    }
}

 

 

Output

{
    "data": {
        "__type": {
            "fields": [{
                "name": "id",
                "description": "The id of a game",
                "type": {
                    "name": "Int"
                }
            }, {
              "name": "title",
                "description": "The title of a game",
                "type": {
                    "name": "String"
                }
            },
            ...
}

 

 

Mutations

Abbiamo parlato di queries, ma spendiamo qualche parola riguardo le altre importanti operazioni che possono essere effettuate tramite un’API, cioè tutte quelle che hanno dei side-effects (scritture sul db, per esempio). GraphQL mette a disposizione un tipo particolare di query, che è sintatticamente identico a quelle viste finora, volto proprio a esplicitare che la query che stiamo eseguendo produrrà dei side effects. Questo tipo di query è chiamato mutation.

 

Esempio #9

mutation {
  updateGameRating(id:2, rating:7) {
    id
    title
    rating
  }
}

 

La sintassi è esattamente identica, ma indichiamo che stiamo eseguendo una mutation cambiando la root della nostra query. In REST, sarebbe equivalente ad eseguire una POST o una PATCH. In questo esempio stiamo assegnando il valore 7 come rating di un oggetto di tipo Game. La mutation può essere definita in questo modo:

{
  name: 'UpdateGameRating',
  type: Game,
  args: {
      rating: { type: new GraphQLNonNull(GraphQLInt) },
    }
}

 

Come si può vedere, il tipo di ritorno della mutation è Game: questo significa che stiamo aggiornando un oggetto di quel tipo ma anche che ci verrà restituito un oggetto sempre dello stesso tipo. Questo è utile per costruire una query in lettura se volessimo sapere il risultato dell’operazione o aggiornare il nostro stato locale sul client.

 

Implementazioni

In questo articolo abbiamo fatto riferimento all’implementazione ufficiale in Javascript, ma ci sono diverse implementazioni disponibili per i linguaggi principali.

Il futuro di GraphQL

È difficile dire cosa sarà di GraphQL e come evolverà. Sicuramente c’è un sacco di hype e come ormai dovremmo aver imparato, non è sempre giustificato. Difficilmente riuscirà a sostituire REST, ma nulla ci vieta di creare un layer GraphQL fra il nostro client ed una classica api REST per cominciare la transizione o vedere come funziona in un progetto reale.

 

Alcuni dei punti ancora aperti e non affrontati nella specifica ufficiale:

  • Come affrontare il caching delle risorse? Relay è un progetto estremamente interessante (nato sempre in Facebook).
  • Come gestire l’accesso alle risorse? Più in generale, è estremamente semplice mettere in difficoltà (e magari anche crashare) un server semplicemente mandando una query complessa con diversi fields annidati.

 

Facbeook ha contribuito tantissimo a spingere lo sviluppo Frontend negli ultimi tempi con progetti come React, e GraphQL potrebbe essere un primo passo nel ripensare le best practices riguardanti lo sviluppo di API.

Marco Sampellegrini

Sviluppo soltanto in JS oramai, principalmente Node e React (native). Sono stra intrippato con il functional programming, studio Haskell di nascosto e spero di riuscire a capire cosa sia un funtore prima o poi.