# GraphQL Simple Server
# Requisitos
Usando los módulos npm express (opens new window), graphql-http (opens new window) y graphql (opens new window) complete el código web de la asignación que implementa un pequeño servicio web con una API GraphQL y pruébela usando GraphiQL.
Sustituya express-graphql (opens new window) por graphql-http (opens new window) para actualizar el código.
# Set up
Para hacer esta práctica empezaremos instalando los módulos que necesitamos y luego en index.js
importamos las correspondientes funciones:
const express = require("express")
const { graphqlHTTP } = require("express-graphql")
const { buildSchema } = require("graphql")
2
3
Puede aprovechar cualquier hoja de cálculo que tenga a mano y la exporta a CSV, para usarla como datos de entrada para hacer las pruebas en esta práctica.
Después, puede usar el módulo csvtojson (opens new window) para convertir los datos a un objeto JS.
const csv=require('csvtojson')
const port = process.argv[2] || 4006;
const csvFilePath = process.argv[3] || 'SYTWS-2122.csv'
const data = String(fs.readFileSync(csvFilePath))
2
3
4
Para hacer el parsing del fichero CSV podemos llamar a csv().fromFile(<file>)
o bien puede usar el ejecutable de línea de comandos que provee $ csvtojson [options] <csv file path>
.
async function main () {
let classroom = await csv().fromFile(csvFilePath);
...
}
2
3
4
Esto deja en classroom
un array con las filas del CSV. En este caso de ejemplo, la información sobre las calificaciones de los estudiantes.
Uno de los primeros pasos a la hora de construir un servicio GraphQL es definir el esquema GraphQL usando el lenguaje SDL.
# GraphQL Schema
A GraphQL schema[1] is at the center of any GraphQL server implementation and describes the functionality available to the clients which connect to it. An Schema is written using the Schema Definition Language (SDL)[2], that defines the syntax for writing GraphQL Schemas. It is otherwise known as Interface Definition Language. It is the lingua franca shared for building GraphQL APIs regardless of the programming language chosen.
Here is an example of a GraphQL Schema written in SDL (file aluschema.gql (opens new window)):
type Student {
AluXXXX: String!
Nombre: String!
markdown: String
}
type Query {
students: [ Student ]
student(AluXXXX: String!): Student
}
type Mutation {
addStudent(AluXXXX: String!, Nombre: String!): Student
setMarkdown(AluXXXX: String!, markdown: String!): Student
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In addition to queries and mutations, GraphQL supports a third operation type: subscriptions
Like queries, subscriptions enable you to fetch data. Unlike queries, subscriptions are long-lasting operations that can change their result over time. They can maintain an active connection to your GraphQL server (most commonly via WebSocket), enabling the server to push updates to the subscription's result.
# Types
GraphQL SDL is a typed language.
Every GraphQL service has a query
type and may or may not have a mutation
type.
These types are the same as a regular object type (opens new window), but they are special because they define the entry point of every GraphQL query. It's often called the Root
type or the Query
type.
Types can be Scalar or can be composed as the Student
type in the former example.
GraphQL ships with some scalar types out of the box; Int
, Float
, String
, Boolean
and ID
.
Object types (opens new window), scalars, and enums (opens new window) are the only kinds of types you can define in GraphQL.
# Type modifiers
But when you use the types in other parts of the schema, or in your query variable declarations, you can apply additional type modifiers that affect validation of those values.
- List -
[]
- A list is a type modifier that represents an array of a type. Lists can be nested. For example,[Int]
represents an array of integers, and[[String]]
represents an array of arrays of strings. - The Non-Null type modifier can also be used when defining arguments for a field. For example,
myField: [String!]
means that the list itself can benull
, but it can't have anynull
members.
# null and Error Management
- By default, every type is nullable - it's legitimate to return
null
as any of the scalar types. - The fields whose types have an exclamation mark,
!
, next to them are non-null fields. These are fields that won’t return anull
value when you query them.
The convention is that if there's an error in the GraphQL interpreter while executing a request, the response is still 200
but the server returns a root field called errors
(with subfields like locations
to spot the place), from which the client can extract them, and a root field called data
that has all the requested fields. Any fields with errors have the value null
.
# Interfaces
Interfaces (opens new window) are a way to describe a set of fields that a type must include to implement the interface.
interface Pupil {
AluXXXX: String!
Nombre: String!
}
type Student implements Pupil {
AluXXXX: String!
Nombre: String!
markdown: String
}
2
3
4
5
6
7
8
9
10
This means that any type that implements Pupil
needs to have these exact fields, with these arguments and return types.
# Arguments
Every field on a GraphQL object type can have zero or more arguments (opens new window), for example the query for a student
:
type Query {
students: [ Student ]
student(AluXXXX: String!): Student
}
2
3
4
All arguments are named. Unlike languages like JavaScript where functions take a list of ordered arguments, all arguments in GraphQL are passed by name specifically. In this case, the student
field has one defined argument, AluXXXX
.
Arguments can be either required or optional. When an argument is optional, we can define a default value - For instance if the field AluXXXX
was declared optional in the query we can write s.t. like:
student(AluXXXX: String = 'alu01013090'): Student
if the AluXXXX
argument is not passed, it will be set to alu01013090
by default.
# buildSchema
The function buildSchema
provided by the graphql
module has the signature:
function buildSchema(source: string | Source): GraphQLSchema
Creates a GraphQLSchema object (opens new window) from GraphQL schema language. The schema will use default resolvers[3].
const AluSchema = buildSchema(StringWithMySchemaDefinition)
# Resolvers
A resolver is a function that resolves a value for a type or field in a schema.
A resolver is a function that connects schema fields and types to various backends. Resolvers provide the instructions for turning a GraphQL operation into data.
A resolver can retrieve data from or write data to anywhere, including a SQL, No-SQL, or graph database, a micro-service, and a REST API.
Resolvers can return objects or scalars like Strings, Numbers, Booleans, etc.
- If an Object is returned, execution continues to the next child field.
- If a scalar is returned (typically at a leaf node of the AST), execution completes.
- If
null
is returned, execution halts and does not continue.
# Resolver arguments
Every resolver in every language receives these four arguments:
root
— Result from the previous/parent typeargs
— Arguments provided to the fieldcontext
— a Mutable object that is provided to all resolvers. Basically a means for resolvers to communicate and share informationinfo
— The decorated AST representation of the query or mutation (opens new window)
# Example
To define our resolvers we create now the object root
mapping the schema fields (students
, student
, addStudent
, setMarkdown
) to their corresponding functions:
const root = {
students: () => classroom,
student: ({ AluXXXX }) => {
let result = classroom.find(s => {
return s["AluXXXX"] == AluXXXX
});
return result || null;
},
addStudent: (object, args, context, info) => {
const { AluXXXX, Nombre } = object;
let result = args.classroom.find(s => {
return s["AluXXXX"] == AluXXXX
});
if (!result) {
let alu = { AluXXXX: AluXXXX, Nombre: Nombre, "markdown": "" }
classroom.push(alu)
return alu
}
// Update the student found
result.Nombre = Nombre
return result;
},
setMarkdown: ({ AluXXXX, markdown }) => {
let result = classroom.findIndex(s => s["AluXXXX"] === AluXXXX)
if (result === -1) {
let message = `Student "${AluXXXX}" not found!`
console.error(message);
throw new Error(message) // will be catched by the GraphQL interpreter
}
classroom[result].markdown = markdown
return classroom[result]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Observe how student
sometimes return null
since it is allowed by the schema we have previously set.
# Default resolvers
It’s worth noting that a GraphQL server has built-in default resolvers, so you don’t have to specify a resolver function for every field. A default resolver will look in root to find a property with the same name as the field. An implementation likely looks like this:
export default {
Student: {
AluXXXX: (parent, args, context, info) => parent.AluXXXX,
Nombre: (parent, args, context, info) => parent.Nombre,
markdown: (parent, args, context, info) => parent.markdown
}
}
2
3
4
5
6
7
This is the reason why there was no need to implement the resolvers for these fields.
Typically, fields are executed in the order they appear in the query, but it’s not safe to assume that. Because fields can be executed in parallel, they are assumed to be
- atomic,
- idempotent, and
- side-effect free.
# Error Management in resolvers
When you have an error, you would have to throw an error from the resolvers of your GraphQL server. This can simpy be done by throwing an error from the resolver.
setMarkdown: ({ AluXXXX, markdown }) => {
let result = classroom.findIndex(s => s["AluXXXX"] === AluXXXX)
if (result === -1) {
let message = `Student "${AluXXXX}" not found!`
throw new Error(message) // will be catched by the GraphQL server
}
classroom[result].markdown = markdown
return classroom[result]
}
2
3
4
5
6
7
8
9
In the picture below, you can see how the error produced by the mutation query ponnota("noexiste", "NO APTO")
is returned to the client.
Notice how the server returns
- a root field called
errors
(with subfields likelocations
andpath
to spot the error), from which the client can extract them, and - a root field called
data
that has the fieldsetMarkdown
with valuenull
.
For more info see the references on Error Management
# Parsing, Validation and Interpretation of a GraphQL Query
Every GraphQL query goes through these phases (opens new window):
- Queries are parsed into an abstract syntax tree (or AST). See https://astexplorer.net/ (opens new window)
- Validated: Checks for query correctness and check if the fields exist.
- Interpreted: The runtime walks through the AST,
- Descending from the root of the tree,
- Invoking resolvers,
- Collecting up results, and
- Emiting the final JSON
The picture below shows the stages of a GraphQL query:
# Example
Suppose the query on the left side of the figure:
after parsed we will have an Abstract Syntax Tree (AST) like the one represented in the right side (See the full AST in astexplorer.net (opens new window))
In this example, the root Query type is the entry point to the AST and contains two fields, user
and album
.
- The
user
andalbum
resolvers are usually executed in parallel or in no particular order. - The AST is traversed breadth-first, meaning
user
must be resolved before its childrenname
andemail
are visited. - If the user resolver is asynchronous, the user branch delays until its resolved.
- Once all leaf nodes,
name
,email
,title
, are resolved, execution is complete.
# Validation
Before the interpretation/traversing of the query AST, there is a validation stage (semantic analysis) by the GraphQL compiler, that uses the GraphQL schema to validate the query:
On the left side appears the AST of the schema. On the right side is the query. The validation process checks that the query is correct and that the fields exist in the schema.
# Execution
Here is an overview of the execution process of a simple GraphQL query and the invocations of the resolvers when traversing the AST:
- In the first level the
parent
isnull
because the node is the root of the AST and theargs
is{id: 'abc'}
. ThefetchUserById("abc")
is called and returns something like{ id: "abc", name: "Sarah" }
- In the second level the
parent
is the result of the first level resolver{ id: "abc", name: "Sarah" }
and theargs
is{}
. Because the resolution of the 2nd level is trivial, default resolvers are used
To know more, you can read the Nicolas Burk articles:
- Structure and Implementation of GraphQL Servers (Part I) (opens new window) GraphQL Server Basics: GraphQL Schemas, TypeDefs & Resolvers Explained
- Structure and Implementation of GraphQL Servers (Part II) (opens new window) GraphQL Server Basics: The Network Layer Explained
- Structure and Implementation of GraphQL Servers (Part III) (opens new window)Demystifying the
info
Argument in GraphQL Resolvers
# Starting the express-graphql middleware
Now what remains is to set the express middleware graphqlHTTP
. The picture below shows the
way express middleware works:
The express middleware graphqlHTTP
is provided by the module express-graphql
and it is used to create the GraphQL HTTP server:
app.use(
'/graphql',
graphqlHTTP((request, response, next) => ({
schema: AluSchema,
rootValue: root,
graphiql: {
defaultQuery: data,
headerEditorEnabled: true,
},
context: { classroom: classroom, req: request, res: response }
})),
);
2
3
4
5
6
7
8
9
10
11
12
It has the following properties:
- schema, our GraphQL schema
- rootValue, our resolver functions
- graphiql, It can be a boolean stating whether to use graphiql (opens new window), we want that so we pass an object describing the graphiql options (opens new window)
- context, an object that is passed to all resolvers and can be used to contain per-request state, such as authentication information, dataloaders, etc.
# Running and Testing the example with GraphiQL
We can now run the app with
npm start
ornodemon index.js [port]
.open the browser at the url
http://localhost:[port]/graphql
to make graphql queries using GraphiQL.Move the JSON at the end of the Query panel to the Query Variables panel:
{ "teacher": "crguezl", "nota": "NO APTO", "myId": "aluNuevo", "id1": "alu0101228587", "id2": "Alu0101232812" }
1
2
3
4
5
6
7
# Fragments
The query panel contains an example of use of fragments. A fragment is basically a reusable piece of query. In GraphQL, you often need to query for the same data fields in different queries.
fragment studentInfo on Student {
Nombre
AluXXXX
}
query ctrlBarra($id1: String!, $id2: String!) {
# fragment example
left: student(AluXXXX: $id1) {
... studentInfo
}
right: student(AluXXXX: $id2) {
... studentInfo
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Exercise: Update your solution to use graphql-http or graphql-yoga
DANGER
express-graphql
was the first official reference implementation of using GraphQL with HTTP. It has existed since 2015 and was mostly unmaintained in recent years.
The official GraphQL over HTTP (opens new window) work group is standardizing the way you transport GraphQL over HTTP and it made great progress bringing up the need for a fresh reference implementation.
Read the GraphQL over HTTP spec (opens new window) for detailed implementation information.
Update your solution to use either
- graphql-http (opens new window), which is now the GraphQL official reference implementation of the GraphQL over HTTP spec (opens new window) or
- GraphQL Yoga (opens new window)
# GraphQL Yoga example
Here is an example with GraphQL Yoga and express:
import express from 'express'
import { createYoga, createSchema } from 'graphql-yoga'
const app = express()
const users = [ { id: '1', login: 'alice' }, { id: '2', login: 'bob' } ]
const simpleSchema = createSchema({
typeDefs: /* GraphQL */ `
type User {
id: ID!
login: String!
}
type Query {
user(byId: ID!): User!
}
`,
resolvers: {
Query: {
user: async (_, args) => {
const user = users.find((user) => user.id === args.byId)
if (!user) {
throw new GraphQLError(`User with id '${args.byId}' not found.`)
}
return user
}
}
}
})
const yoga = createYoga({
schema: simpleSchema,
graphiql: {
defaultQuery: /* GraphQL */ `
query {
user(byId: 1) {
login
}
}
`
}
})
// Bind GraphQL Yoga to `/graphql` endpoint
app.use('/graphql', yoga)
app.listen(4000, () => {
console.log('Running a GraphQL API server at http://localhost:4000/graphql')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
See file graphql-yoga-examples/graphql-yoga-express/index.js (opens new window)
# graphql-http example
Here is an example of usage of graphql-http
with express:
import express from 'express';
import { createHandler } from 'graphql-http/lib/use/express';
import { schema } from './previous-step';
// Create a express instance serving all methods on `/graphql`
// where the GraphQL over HTTP express request handler is
const app = express();
app.all('/graphql', createHandler({ schema }));
app.listen({ port: 4000 });
console.log('Listening to port 4000');
2
3
4
5
6
7
8
9
10
11
# GraphQL Exercises
Study the examples GraphQL Hello Worlds at (opens new window) https://graphql.org/code/#javascript (opens new window) Graphql.js and Apollo Server.
# References
# Introduction to GraphQL
- See inside the repo crguezl/simple-graphql-express-server-example (opens new window) the folder
simple-graphql-express-server-example/
with the example used in this description - GraphQL Glossary (opens new window)
- GraphQL Hello World (opens new window). A YouTube list of videos by Ben Awad
- GraphQL fragments explained (opens new window)
- GraphQL Resolvers: Best Practices (opens new window) by Mark Stuart
- Youtube video GraphQL Tutorial. Nos montamos una API con Nodejs y Express (opens new window)
# Error Management
# Express-GraphQL
# GraphiQL
- Queries y GraphiQL con la API de Rick & Morty (Curso express GraphQL)
- GraphiQL Shortcuts (opens new window)
- Express-GraphQL: graphiql options (opens new window)
# Parsing, Validation and Execution
- Life of a GraphQL Query by Christian Joudrey
- GraphQL Specification (opens new window)
- graphql-js (opens new window) the JavaScript reference implementation for GraphQL
- Advanced GraphQL Patterns: Embrace the AST! (opens new window) Overcoming the Fear of Apollo Server Internals. Nick Redmark
- https://astexplorer.net/ (opens new window)
# Template repo
# FootNotes
For more detail on the GraphQL schema language, see the schema language docs (opens new window) ↩︎