Master de II. ULL. 1er cuatrimestre
Usando los módulos npm express, express-graphql y graphql escriba un servicio web con una API GraphQL y pruébela usando GraphiQL.
Para hacer esta práctica empezaremos instalando los módulos que necesitamos y luego en index.js
importamos las correspondientes funciones:
1
2
3
const express = require("express")
const { graphqlHTTP } = require("express-graphql")
const { buildSchema } = require("graphql")
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 para convertir los datos a un objeto JS.
1
2
3
4
const csv=require('csvtojson')
const port = process.argv[2] || 4006;
const csvFilePath = process.argv[3] || 'SYTWS-2122.csv'
const data = String(fs.readFileSync(csvFilePath))
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>
.
1
2
3
4
async function main () {
let classroom = await csv().fromFile(csvFilePath);
...
}
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.
A GraphQL schema1 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:
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
}
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.
GraphQL SDL is a typed language. 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
.
The fields whose types have an exclamation mark, !
, next to them are non-null fields. These are fields that won’t return a null
value when you query them.
The function buildSchema
provided by the graphql
module has the signature:
1
function buildSchema(source: string | Source): GraphQLSchema
Creates a GraphQLSchema object from GraphQL schema language. The schema will use default resolvers3.
1
const AluSchema = buildSchema(StringWithMySchemaDefinition)
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 also return strings, ints, null, and other types.
To define our resolvers we create now the object root
mapping the schema fields (students
, student
, addStudent
, setMarkdown
) to their corresponding functions:
1
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
async function main () {
let classroom=await csv().fromFile(csvFilePath);
const root = {
students: () => classroom,
student: ({AluXXXX}) => {
let result = classroom.find(s => {
return s["AluXXXX"] == AluXXXX
});
return result
},
addStudent: (obj, args, context, info) => {
const {AluXXXX, Nombre} = obj;
let result = classroom.find(s => {
return s["AluXXXX"] == AluXXXX
});
if (!result) {
let alu = {AluXXXX : AluXXXX, Nombre: Nombre}
console.log(`Not found ${Nombre}! Inserting ${AluXXXX}`)
classroom.push(alu)
return alu
}
return result;
},
setMarkdown: ({AluXXXX, markdown}) => {
let result = classroom.findIndex(s => s["AluXXXX"] === AluXXXX)
if (result === -1) {
let message = `${AluXXXX} not found!`
console.log(message);
return null;
}
classroom[result].markdown = markdown
return classroom[result]
}
}
... // Set the express app to work
}
main();
Observe how setMarkDown
and addStudent
sometimes return null
since it is allowed by the schema we have previously set.
setMarkdown(AluXXXX: String!, markdown: String!): Student
There is no exclamation !
at the value returned in the declaration of the setMarkDown
mutation.
Every GraphQL query goes through these phases:
![]() |
![]() |
In this example, the root Query type is the entry point to the AST and contains two fields, user
and album
. The user
and album
resolvers are usually executed in parallel or in no particular order.
The AST is traversed breadth-first, meaning user
must be resolved before its children name
and email
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.
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.
A resolver is a function that resolves a value for a type or field in a schema.
null
is returned, execution halts and does not continue.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:
1
2
3
4
5
6
7
8
export default {
Student: {
AluXXXX: (root, args, context, info) => root.AluXXXX,
Nombre: (root, args, context, info) => root.Nombre,
markdown: (root, args, context, info) => root.markdown
}
}
This is the reason why there was no need to implement the resolvers for these fields.
Now what remains is to set the graphqlHTTP
the express middleware provided by the module express-graphql
to work
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const app = express()
async function main () {
let classroom = await csv().fromFile(csvFilePath);
const root = { ... }
app.use(
'/graphql',
graphqlHTTP({
schema: AluSchema,
rootValue: root,
graphiql: true,
}),
);
app.listen(port);
console.log("Running at port "+port)
}
It has the following properties:
We can now run the app and open the browser at the url http://localhost:4000/graphql
to make graphql queries using GraphiQL.
Use GraphiQL to test your API. GraphiQL is an in-browser IDE for GraphQL development and workflow. Para ello vea este video:
See inside the repo crguezl/learning-graphql-with-gh the folder simple-graphql-express-server-example/
with the example used in this description
For more detail on the GraphQL schema language, see the schema language docs ↩