GraphQL vs. REST - Un tutorial GraphQL
Pubblicato: 2022-03-11Potresti aver sentito parlare del nuovo ragazzo in giro per l'isolato: GraphQL. In caso contrario, GraphQL è, in una parola, un nuovo modo di recuperare le API, un'alternativa a REST. È iniziato come un progetto interno a Facebook e, da quando era open source, ha guadagnato molta trazione.
Lo scopo di questo articolo è aiutarti a passare facilmente da REST a GraphQL, sia che tu abbia già deciso per GraphQL o che tu sia solo disposto a provarlo. Non è necessaria alcuna conoscenza preliminare di GraphQL, ma per comprendere l'articolo è necessaria una certa familiarità con le API REST.
La prima parte dell'articolo inizierà fornendo tre motivi per cui personalmente penso che GraphQL sia superiore a REST. La seconda parte è un tutorial su come aggiungere un endpoint GraphQL al tuo back-end.
Graphql vs. REST: perché eliminare REST?
Se stai ancora esitando sul fatto che GraphQL sia adatto o meno alle tue esigenze, qui viene fornita una panoramica piuttosto ampia e obiettiva di "REST vs. GraphQL". Tuttavia, per i miei tre principali motivi per utilizzare GraphQL, continua a leggere.
Motivo 1: prestazioni di rete
Supponi di avere una risorsa utente sul back-end con nome, cognome, e-mail e altri 10 campi. Sul client, generalmente ne servono solo un paio.
Effettuare una chiamata REST sull'endpoint /users
restituisce tutti i campi dell'utente e il client utilizza solo quelli di cui ha bisogno. C'è chiaramente uno spreco di trasferimento di dati, che potrebbe essere una considerazione sui client mobili.
GraphQL per impostazione predefinita recupera i dati più piccoli possibili. Se hai bisogno solo del nome e del cognome dei tuoi utenti, specificalo nella tua query.
L'interfaccia seguente si chiama GraphiQL, che è come un esploratore di API per GraphQL. Ho creato un piccolo progetto per lo scopo di questo articolo. Il codice è ospitato su GitHub e lo approfondiremo nella seconda parte.
Nel riquadro sinistro dell'interfaccia c'è la query. Qui, stiamo recuperando tutti gli utenti - faremmo GET /users
con REST - e ottenendo solo il loro nome e cognome.
Domanda
query { users { firstname lastname } }
Risultato
{ "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }
Se volessimo ricevere anche le e-mail, l'aggiunta di una riga "email" sotto "cognome" farebbe il trucco.
Alcuni back-end REST offrono opzioni come /users?fields=firstname,lastname
per restituire risorse parziali. Per quel che vale, Google lo consiglia. Tuttavia, non è implementato per impostazione predefinita e rende la richiesta appena leggibile, specialmente quando si lanciano altri parametri di query:
-
&status=active
per filtrare gli utenti attivi -
&sort=createdAat
per ordinare gli utenti in base alla data di creazione -
&sortDirection=desc
perché ovviamente ne hai bisogno -
&include=projects
per includere i progetti degli utenti
Questi parametri di query sono patch aggiunte all'API REST per imitare un linguaggio di query. GraphQL è soprattutto un linguaggio di interrogazione, che rende sin dall'inizio le richieste concise e precise.
Motivo 2: la scelta progettuale "Includi vs. Endpoint".
Immaginiamo di voler costruire un semplice strumento di gestione dei progetti. Abbiamo tre risorse: utenti, progetti e attività. Definiamo inoltre le seguenti relazioni tra le risorse:
Ecco alcuni degli endpoint che esponiamo al mondo:
Punto finale | Descrizione |
---|---|
GET /users | Elenca tutti gli utenti |
GET /users/:id | Ottieni il singolo utente con id :id |
GET /users/:id/projects | Ottieni tutti i progetti di un utente |
Gli endpoint sono semplici, facilmente leggibili e ben organizzati.
Le cose si complicano quando le nostre richieste diventano più complesse. Prendiamo l'endpoint GET /users/:id/projects
: diciamo che voglio mostrare solo i titoli dei progetti nella home page, ma i progetti+attività nella dashboard, senza effettuare più chiamate REST. io chiamerei:
-
GET /users/:id/projects
per la home page. -
GET /users/:id/projects?include=tasks
(ad esempio) nella pagina del dashboard in modo che il back-end aggiunga tutte le attività correlate.
È pratica comune aggiungere parametri di query ?include=...
per farlo funzionare ed è persino consigliato dalla specifica dell'API JSON. I parametri di query come ?include=tasks
sono ancora leggibili, ma in breve tempo ci ritroveremo con ?include=tasks,tasks.owner,tasks.comments,tasks.comments.author
.
In questo caso, sarebbe più saggio creare un endpoint /projects
per farlo? Qualcosa come /projects?userId=:id&include=tasks
, dato che avremmo un livello di relazione in meno da includere? Oppure, in realtà, potrebbe funzionare anche un /tasks?userId=:id
endpoint. Questa può essere una scelta progettuale difficile, ancora più complicata se abbiamo una relazione molti-a-molti.
GraphQL utilizza l'approccio include
ovunque. Ciò rende la sintassi per recuperare le relazioni potente e coerente.
Ecco un esempio di recupero di tutti i progetti e le attività dall'utente con ID 1.
Domanda
{ user(id: 1) { projects { name tasks { description } } } }
Risultato
{ "data": { "user": { "projects": [ { "name": "Migrate from REST to GraphQL", "tasks": [ { "description": "Read tutorial" }, { "description": "Start coding" } ] }, { "name": "Create a blog", "tasks": [ { "description": "Write draft of article" }, { "description": "Set up blog platform" } ] } ] } } }
Come puoi vedere, la sintassi della query è facilmente leggibile. Se volessimo approfondire e includere attività, commenti, immagini e autori, non ci penseremmo due volte su come organizzare la nostra API. GraphQL semplifica il recupero di oggetti complessi.
Motivo 3: gestione di diversi tipi di clienti
Quando costruiamo un back-end, iniziamo sempre cercando di rendere l'API il più ampiamente utilizzabile possibile da tutti i client. Eppure i clienti vogliono sempre chiamare di meno e recuperare di più. Con inclusioni profonde, risorse parziali e filtri, le richieste effettuate da client Web e mobili possono differire molto l'una dall'altra.
Con REST, ci sono un paio di soluzioni. Possiamo creare un endpoint personalizzato (ovvero un endpoint alias, ad esempio /mobile_user
), una rappresentazione personalizzata ( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json
) o anche un client -API specifica (come faceva una volta Netflix). Tutti e tre richiedono uno sforzo aggiuntivo da parte del team di sviluppo back-end.
GraphQL offre più potenza al client. Se il cliente ha bisogno di richieste complesse, costruirà le query corrispondenti. Ogni client può utilizzare la stessa API in modo diverso.
Come iniziare con GraphQL
Nella maggior parte dei dibattiti su "GraphQL vs. REST" oggi, le persone pensano di dover scegliere uno dei due. Questo semplicemente non è vero.
Le applicazioni moderne generalmente utilizzano diversi servizi diversi, che espongono diverse API. Potremmo effettivamente pensare a GraphQL come un gateway o un wrapper per tutti questi servizi. Tutti i client raggiungerebbero l'endpoint GraphQL e questo endpoint raggiungerebbe il livello del database, un servizio esterno come ElasticSearch o Sendgrid o altri endpoint REST.
Un secondo modo per utilizzare entrambi consiste nell'avere un endpoint /graphql
separato sull'API REST. Ciò è particolarmente utile se hai già numerosi client che colpiscono la tua API REST, ma vuoi provare GraphQL senza compromettere l'infrastruttura esistente. E questa è la soluzione che stiamo esplorando oggi.
Come detto in precedenza, illustrerò questo tutorial con un piccolo progetto di esempio, disponibile su GitHub. È uno strumento di gestione dei progetti semplificato, con utenti, progetti e attività.
Le tecnologie utilizzate per questo progetto sono Node.js ed Express per il server web, SQLite come database relazionale e Sequelize come ORM. I tre modelli (utente, progetto e attività) sono definiti nella cartella models
. Gli endpoint REST /api/users
, /api/projects
e /api/tasks
sono esposti al mondo e sono definiti nella cartella rest
.

Si noti che GraphQL può essere installato su qualsiasi tipo di back-end e database, utilizzando qualsiasi linguaggio di programmazione. Le tecnologie qui utilizzate sono scelte per motivi di semplicità e leggibilità.
Il nostro obiettivo è creare un endpoint /graphql
senza rimuovere gli endpoint REST. L'endpoint GraphQL colpirà direttamente l'ORM del database per recuperare i dati, in modo che sia totalmente indipendente dalla logica REST.
Tipi
Il modello di dati è rappresentato in GraphQL da tipi , fortemente tipizzati. Dovrebbe esserci una mappatura 1 a 1 tra i tuoi modelli e i tipi GraphQL. Il nostro tipo di User
sarebbe:
type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }
Interrogazioni
Le query definiscono quali query puoi eseguire sulla tua API GraphQL. Per convenzione, dovrebbe esserci un RootQuery
, che contiene tutte le query esistenti. Ho anche indicato l'equivalente REST di ogni query:
type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users project(id: ID!): Project # Corresponds to GET /api/projects/:id projects: [Project] # Corresponds to GET /api/projects task(id: ID!): Task # Corresponds to GET /api/tasks/:id tasks: [Task] # Corresponds to GET /api/tasks }
Mutazioni
Se le query sono richieste GET
, le mutazioni possono essere viste come richieste POST
/ PATCH
/ PUT
/ DELETE
(sebbene in realtà siano versioni sincronizzate delle query).
Per convenzione, mettiamo tutte le nostre mutazioni in una RootMutation
:
type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createProject(input: ProjectInput!): Project updateProject(id: ID!, input: ProjectInput!): Project removeProject(id: ID!): Project createTask(input: TaskInput!): Task updateTask(id: ID!, input: TaskInput!): Task removeTask(id: ID!): Task }
Si noti che qui abbiamo introdotto nuovi tipi, chiamati UserInput
, ProjectInput
e TaskInput
. Questa è una pratica comune anche con REST, per creare un modello di dati di input per la creazione e l'aggiornamento delle risorse. Qui, il nostro tipo UserInput
è il nostro tipo User
senza i campi id
e projects
e nota la parola chiave di input
anziché il type
:
input UserInput { firstname: String lastname: String email: String }
Schema
Con tipi, query e mutazioni, definiamo lo schema GraphQL , che è ciò che l'endpoint GraphQL espone al mondo:
schema { query: RootQuery mutation: RootMutation }
Questo schema è fortemente tipizzato ed è ciò che ci ha permesso di avere quei pratici completamenti automatici in GraphiQL.
Risolutori
Ora che abbiamo lo schema pubblico, è il momento di dire a GraphQL cosa fare quando viene richiesta ciascuna di queste query/mutazioni. I risolutori fanno il duro lavoro; possono, ad esempio:
- Raggiungi un endpoint REST interno
- Chiama un microservizio
- Premi il livello del database per eseguire operazioni CRUD
Stiamo scegliendo la terza opzione nella nostra app di esempio. Diamo un'occhiata al nostro file di risoluzione:
const models = sequelize.models; RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); }, // Resolvers for Project and Task go here }, /* For reminder, our RootQuery type was: type RootQuery { user(id: ID): User users: [User] # Other queries }
Ciò significa che, se la query user(id: ID!)
viene richiesta su GraphQL, restituiremo User.findById()
, che è una funzione Sequelize ORM, dal database.
Che ne dici di unire altri modelli nella richiesta? Bene, dobbiamo definire più risolutori:
User: { projects (user) { return user.getProjects(); // getProjects is a function managed by Sequelize ORM } }, /* For reminder, our User type was: type User { projects: [Project] # We defined a resolver above for this field # ...other fields } */
Quindi, quando richiediamo il campo projects
in un tipo User
in GraphQL, questo join verrà aggiunto alla query del database.
E infine, i risolutori per le mutazioni:
RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Project and Task go here }
Puoi giocare con questo qui. Per mantenere puliti i dati sul server, ho disabilitato i resolver per le mutazioni, il che significa che le mutazioni non eseguiranno alcuna operazione di creazione, aggiornamento o eliminazione nel database (e quindi restituiranno null
sull'interfaccia).
Domanda
query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }
Risultato
{ "data": { "user": { "firstname": "Alicia", "lastname": "Smith", "projects": [ { "name": "Email Marketing Campaign", "tasks": [ { "description": "Get list of users" }, { "description": "Write email template" } ] }, { "name": "Hire new developer", "tasks": [ { "description": "Find candidates" }, { "description": "Prepare interview" } ] } ] } } }
La riscrittura di tutti i tipi, query e risolutori dell'app esistente potrebbe richiedere del tempo. Tuttavia, esistono molti strumenti per aiutarti. Ad esempio, esistono strumenti che traducono uno schema SQL in uno schema GraphQL, inclusi i risolutori!
Mettere tutto insieme
Con uno schema ben definito e risolutori su cosa fare su ogni query dello schema, possiamo montare un endpoint /graphql
sul nostro back-end:
// Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));
E possiamo avere un'interfaccia GraphiQL di bell'aspetto sul nostro back-end. Per effettuare una richiesta senza GraphiQL, copia semplicemente l'URL della richiesta ed eseguilo con cURL, AJAX o direttamente nel browser. Naturalmente, ci sono alcuni client GraphQL per aiutarti a creare queste query. Vedi sotto per alcuni esempi.
Qual è il prossimo?
Lo scopo di questo articolo è darti un assaggio dell'aspetto di GraphQL e mostrarti che è sicuramente possibile provare GraphQL senza buttare via la tua infrastruttura REST. Il modo migliore per sapere se GraphQL è adatto alle tue esigenze è provarlo tu stesso. Spero che questo articolo ti farà fare il tuffo.
Ci sono molte funzionalità di cui non abbiamo discusso in questo articolo, come aggiornamenti in tempo reale, batch lato server, autenticazione, autorizzazione, memorizzazione nella cache lato client, caricamento file, ecc. Un'ottima risorsa per conoscere queste funzionalità è Come GraphQL.
Di seguito alcune altre risorse utili:
Strumento lato server | Descrizione |
---|---|
graphql-js | L'implementazione di riferimento di GraphQL. Puoi usarlo con express-graphql per creare un server. |
graphql-server | Un server GraphQL all-in-one creato dal team Apollo. |
Implementazioni per altre piattaforme | Ruby, PHP, ecc. |
Strumento lato client | Descrizione |
---|---|
Relè | Un framework per connettere React con GraphQL. |
apollo-cliente. | Un client GraphQL con collegamenti per React, Angular 2 e altri framework front-end. |
In conclusione, credo che GraphQL sia più di un clamore. Non sostituirà ancora REST domani, ma offre una soluzione efficiente a un problema reale. È relativamente nuovo e le migliori pratiche sono ancora in via di sviluppo, ma è sicuramente una tecnologia di cui sentiremo parlare nei prossimi due anni.