DevLeague Coding Bootcamp
DevLeague is a Full Stack Coding Bootcamp
A Query Language for your API
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
The main idea is that you can specify what data you want and how it looks from the frontend.
All GraphQL is responsible for is to get the requested data and format it properly
{
"data": {
"hero": {
"name": "Luke Skywalker"
}
}
}
{
hero {
name
}
}
The client ask for this...
Server returns this...
This works because the backend server has a "schema" that defines what the data can look like.
As long as the frontend asks for some subset of that "schema", the backend server can "resolve" what the client is asking for.
const Hero = new graphql.GraphQLObjectType({
name: 'Hero',
fields: () => ({
id: { type: graphql.GraphQLString },
name: { type: graphql.GraphQLString },
secret_identity: { type: graphql.GraphQLString },
}),
});
Example schema for Hero:
Once a schema is defined and a request for data reaches the server, it's the job of the "resolver" to figure out how to retrieve that data.
const graphql = require('graphql');
const models = require('../models');
const knex = require('../../../knex');
module.exports = {
type: models.Hero,
args: { id: { type: graphql.GraphQLNonNull(graphql.GraphQLInt) } },
resolve: async (parent, args, context, resolveInfo) => {
const q = 'SELECT * from heros WHERE id = ?';
const hero = (await knex.raw(q, [args.id])).rows[0];
return hero;
},
};
Resolvers can be nested to retrieve nested data as well.
Here's the model for a super team:
const graphql = require('graphql');
const models = require('../models');
const knex = require('../../../knex');
const Team = new graphql.GraphQLObjectType({
name: 'Team',
fields: () => ({
id: { type: graphql.GraphQLInt },
name: { type: graphql.GraphQLString },
heroes: {
type: graphql.GraphQLList(Hero),
resolve: async (team) => {
const q = 'SELECT * from heros WHERE team_id = ?';
const result = await knex.raw(q, [team.id]);
const heroes = result.rows;
return heroes;
},
},
}),
});
Given this request from the client:
{
team {
id,
name,
heroes
}
}
{
data: {
team: {
id: 1,
name: "Avengers",
heroes: [
{
id: 1,
name: "Iron Man"
secret_identity: "Tony Stark"
},
{
id: 2,
name: "Captain America"
secret_identity: "Steve Rogers"
},
{
id: 3,
name: "Hulk"
secret_identity: "Bruce Banner"
},
{
id: 1,
name: "Thor"
secret_identity: "Thor"
},
}
}
Along with querying for data from the backend, mutations allow us to "mutate" or modify the data.
const graphql = require('graphql');
const models = require('../models');
const knex = require('../../../knex');
module.exports = {
type: models.Hero,
args: {
name: { type: graphql.GraphQLNonNull(graphql.GraphQLString) },
secret_identity: { type: graphql.GraphQLNonNull(graphql.GraphQLString) },
},
resolve: async (parent, args, context, resolveInfo) => {
const q = 'INSERT INTO heroes (name, secret_identity) VALUES (?, ?) RETURNING *';
const params = [args.name, args.secret_identity];
const result = await knex.raw(q, params);
const hero = result.rows[0];
return hero;
},
};
Mutations can be sent from the client side like this:
mutation NewHero {
createHero(name:"Hawkeye", secret_identity:"Clint Barton") {
id,
name,
secret_identity
}
}
Since both mutations and queries can ask for nested data, each nested call will generate a new unique call to gather that nested data from the database.
This is known as the n+1 problem. Where n is the number of nested data resources plus the original query itself.
We can solve the N+1 problem by using data "batching"
Or otherwise, creating queries that allow us to batch similar data request to the database all into one call.
const loadTeamHeroes = (teamIds) => {
return knex
.table('heroes')
.whereIn('team_id', teamIds)
.select()
.then((rows) => teamIds.map((id) => rows.filter((hero) => hero.team_id === id)));
};
Now, there will only be two calls to the database:
1.) The query for the team itself
2.) The query for all players on that team
{
team {
id,
name,
heroes
}
}
This originally generated 5 queries. 1 for the team itself, then 4 for each of the heroes in this team: Iron Man, Captain America, Hulk, and Thor.
{
team {
id,
name,
heroes
}
}
With batching there are only two queries:
1 for the team itself, and another to get all the heroes for this team as an array.
In the end, the client ends up with the same data but the number of call to the DB is drastically reduced.
Given the flexibility of the query from the client side, batching by hand would be difficult since every condition where a model is nested will need their own "batching" resolver.
There are multiple strategies to handle this:
DataLoader
JoinMonster
Both offer batching as well as caching the data so frequent requests for the same data aren't executed multiple times per request.
Queries don't look like regular JSON.
To send a query or mutation from the client side, you can use the apollo client library.
All queries and mutations to GraphQL need to be done with POST since the body where the query is defined.
POST body:
{
"query": "query hello { team(id: 1) { id, name, heroes { id, name } } }"
}
A client isn't required and if it seems easier, queries and mutations can be written with just plain JS and strings.
(async function() {
const req = await fetch("http://localhost:3000/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `{
hero: {
id,
name
}
}`
})
});
})();
By DevLeague Coding Bootcamp
An Intro to GraphQL