GraphQL Server基础概念
GraphQL Server
https://itnext.io

In this article you will learn concepts like schema and resolver and finally you will learn how to create a GraphQL server and query the same. This covers the necessary basics.

There are different ways of defining a schema. Use the one that works for you

  • Raw approach, this uses the primitives exposed by the graphql library

  • GraphQL query language definition, here we will use the method buildSchema(), this is way more easy to work with


The raw approach is meant to show you what goes on under the hood. If you only want to know how to build your schemas with Query language definition , then jump to thebuildSchema section in this article

Schema Content

The Schema can be said to consist of two parts:

  • Schema definition, by defining a schema we define the entities that exist but also the different queries and mutations that are possible to make

  • Resolvers, resolvers are functions that are invoked when the user makes a query or mutation. Resolvers talk to a 3rd party API or a database to perform an operation and ends up returning an answer back to the calling user

Schema definition

A schema pretty much contains the following concepts:

  • Query, this is queryable data, it is either parameterless or takes a parameter

  • Custom types, you can build your own types if you are not happy with the basic types that is being provided

  • Mutations, these are “methods” that should change the data like CREATE, UPDATE or DELETE

  • Resolvers, these are functions returning data back to the user

  • Fragment, this is a reusable piece of schema that you can use in many places in your definition

  • Alias, this is a rename mechanism that allows us to rename a column

  • Directive, think of this one as if/else and it allows you to decide what columns to include/exclude in your query

Resolvers

Resolvers are part of your schema instance but not part of the Schema definition. Resolvers are simply functions that responds when you try to call a query or mutation. A resolver function responds either with data directly or with a Promise.

The raw approach

In the raw approach we are about to describe below, the schema definition and the resolvers are baked in together. We will define a resource we can call and its corresponding resolver, in one definition.

To create a schema you need to create an instance of the type GraphQLSchema. There are different ways of creating this instance but let’s look at the most raw approach first. In this approach we have the following type GraphQLSchema:

new GraphQLSchema(options)

options is of type GraphQLSchemaConfig and looks like below:

export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions {
 query: Maybe<GraphQLObjectType>;
 mutation?: Maybe<GraphQLObjectType>;
 subscription?: Maybe<GraphQLObjectType>;
 types?: Maybe<GraphQLNamedType[]>;
 directives?: Maybe<GraphQLDirective[]>;
 astNode?: Maybe<SchemaDefinitionNode>;
 extensionASTNodes?: Maybe<ReadonlyArray<SchemaExtensionNode>>;
}

As we can see above, the only mandatory field we need to give it for now is query. This is simple queries/questions and we can see that those are of type GraphQLObjectType. So let’s start constructing such an instance:

import {
 graphql,
 GraphQLSchema,
 GraphQLObjectType,
 GraphQLString,
} from “graphql”;

const schema = new GraphQLObjectType({
 name: “RootQueryType”,
 fields: {
   hello: {
     type: GraphQLString,
     resolve() {
       return “world”;
     }
   }
 }
});

Above we can see that we have created the query hello. hello is assigned an object so let’s break that object down:

  • type, this is the datatype, either a primitive or a custom type

  • resolve, this is a resolve function that simply says, if someone invokes me, what should I answer with

There are more fields that we could be declaring like description but these two are the most important.

Querying our schema

Next step is to tell graphql about this schema so we can use it:

let query = `{ hello }`;

graphql(schema, query).then(result => {
 res.json(result);
});

Above you can see that we invoke graphql with two different parameters:

  • schema, the schema we just defined

  • query, this is a variable we just initialized and it contains the GraphQL query language

Let’s zoom in on the query and see what it does:

{
 hello
}

Above we simply query for a known query hello and this will respond with world as querying for it will invoke the resolve() method. Ok great, we have just created a hello world in GraphQL. Pause for effect :)

Introducing a custom type

Now there are a lot of primitives we could be using in GraphQL and we have just looked at a String type. There comes a time when that is really limiting and we want to construct our own types. So let’s do that next:

let humanType = new GraphQLObjectType({
 name: "Human",
 fields: () => ({
   id: { type: GraphQLString },
   description: { type: GraphQLString },
   name: { type: GraphQLString }
 })
});

Above we are using the GraphQLObjectType to create our custom type. The property fields allows us to list all the different properties our type should have like id, description and name.

Ok so we have a custom type, let’s put it into use by using it in our schema like so:

let schema = new GraphQLSchema({
 query: new GraphQLObjectType({
   name: “RootQueryType”,
   fields: {
     hello: {
       type: GraphQLString,
       resolve() {
         return “world”;
       }
     },
     person: {
       type: humanType,
       resolve() {
         return people[0];
       }
     }
   }
 })
});

Above we added person that has the type humanType and is thereby using our custom type. Because we added person as a queryable we can now extend our query with this type, like so:

let query = `{ hello, person { name, description } }`;
graphql(schema, query).then(result => {
 res.json(result);
});

As you can see above we are now adding the following:

{
 person {
   name, description
 }
}

We are querying for person and we know that person is of type humanTypeand humanType has the fields id, name and description and we choose to select for a subset of that, namely name and description.

Full code so far looks like this:

const people = [
 {
   id: 1,
   name: “chris”,
   description: “viking”
 },
 {
   id: 2,
   name: “maxim”,
   description: “viking”
 },
 {
   id: 3,
   name: “sherry”,
   description: “viking”
 },
 {
   id: 4,
   name: “ana”,
   description: “viking”
 }
];
let humanType = new GraphQLObjectType({
 name: "Human",
 fields: () => ({
   id: { type: GraphQLString },
   description: { type: GraphQLString },
   name: { type: GraphQLString }
 })
});
let schema = new GraphQLSchema({
 query: new GraphQLObjectType({
   name: "RootQueryType",
   fields: {
     hello: {
       type: GraphQLString,
       resolve() {
         return "world";
       }
     },
     person: {
       type: humanType,
       resolve() {
         return people[0];
       }
     }
   }
})
});
let query = `{ hello, person { name, description } }`;
graphql(schema, query).then(result => {
 res.json(result);
});

Introducing a list type

Ok, so far we have described how we can use a primitive like a String but also how we can create a custom type humanKind. Let’s look at how we can use the list type. To use a list type we need to create something of type GraphQLList. Let’s add that to our existing schema, like so:

let schema = new GraphQLSchema({
 query: new GraphQLObjectType({
   name: "RootQueryType",
   fields: {
     hello: {
       type: GraphQLString,
       resolve() {
         return "world";
       }
     },
     person: {
       type: humanType,
       resolve() {
         return people[0];
       }
     },
     people: {
       type: new GraphQLList(humanType),
       resolve() {
         return people;
       }
     }
   }
})
});

Above we added people that uses GraphQLList and you can see we invoke it like a method and pass our custom type humanType to it to say what type of list this is. That’s really all there is to it.

buildSchema — using query language definition

So far we have been defining a schema in the most painful way possible by working with types like GraphQLObjectType, GraphQLList etc, low level types that forces us to write a lot produce our schema.

Using this approach we will separate the schema definition from the resolvers, so we need to create those separately

Sure it is somewhat readable but there is a better way. That better way is using a method called buildSchema() that will produce an instance of GraphQLSchema and enables us to define the schema in a much more readable way. Let me show you:

import { buildSchema } from ‘graphql’;
var schema = buildSchema(`
 type Query {
   hello: String,
 }
`);

var root = {
 hello: () => { return 'world' }
}

let query = `{ hello, person { name }, people { name, description } }`;

graphql({
 schema,
 source: query,
 rootValue: root
}).then(result => {
 console.log(result);
});

custom type

To create a custom type we simply need to use the type keyword and choose a name for our type, like so:

var schema = buildSchema(`
 type Person {
   id: Int,
   name: String
 },
 type Query {
   hello: String,
   person: Person
 }
`);

What you can see above is that we do two things:

  • create our type Person

  • extend the Query with person that is of type Person

This means that we now need to add a resolver function for person so that if someone tries to query for it, they won’t get an error, so let’s do that next:

const people = [{
 id: 1, name: ‘chris’
},
{
 id: 2, name: ‘maxim’
}]

var root = {
 hello: () => { return ‘world’ },
 person: () => people[0]
}

Above you can see that we created the people array and that we also extended our root, our resolver with the property person. The end result here is that someone can now query for person and it will work

list type

Let’s add another a list type to our schema definition:

var schema = buildSchema(`
 type Person {
   id: Int,
   name: String
 },
 type Query {
   hello: String,
   person: Person,
   people: [Person]
 }
`);

Above we have now added the queryable people and we can see that it is of list type because it is using square brackets [Person]. Thereafter we need to add a resolver function for person that we are used to by now:

var root = {
 hello: () => { return 'world' },
 person: (id) => people.find(p => p.id === id),
 people: () => people
}

parameterized query

So far we have looked at simple queries, with no parameters, but most likely we will need to send some parameters to our query to filter down our response. We can easily do that by first defining our schema correctly. We have an existing person property so let’s update that one to take a parameter, like so:

var schema = buildSchema(`
 type Person {
   id: Int,
   name: String
 },
 type Query {
   hello: String,
   person( id: Int!): Person,
   people: [Person]
 }
`);

person now looks like the following:

person( id: Int!): Person

It looks like the signature of a method call.

Mutation

Mutation is the way we change data in our application so we can perform operations like CREATE, UPDATE or DELETE on a resource. To define a mutation we simply need to create the reserved type Mutation, like so:

type Mutation {
 // mutation
 // another mutation
}

Once we have the above we can start thinking about what things we want to do. Given the above narrative with a peoples list it makes sense to be able to add a person to that list, so let’s create such a mutation, so we update our schema to the following:

input PersonInput {
 name: String!
},
type Mutation {
 addPerson(person: PersonInput!): String
}

The above tells us we are able to invoke a method/mutation called addPerson() that takes a person as an input parameter and ends up responding with a string. We are adding a new type of construct here of type input. When dealing with mutations we need to construct things of type input if we want to send something more complicated than a primitive ( String, Boolean etc ). Yes our PersonInput looks a bit simple and a primitive might have been enough in this case but I opted to show you how you could have sent a more complicated input instead, should you wish it.

Next step is to define a resolver for this so that if someone invokes addPerson() we know what to do:

const people = [{
 id: 1,
 name: 'chris'
}]
const addPerson = (person) => {
const nextId = people.length === 0 ? 1 : people[people.length -1].id + 1
people = […people, {…person, { id : nextId } }]
}
var root = {
 hello: () => { return 'world' },
 person: (id) => people.find(p => p.id === id),
 people: () => people,
 addPerson: (args) => {
   const { person } = args;
   addPerson(person);
   return 'success'
 }
}

Add this point we have created a specific mutation addPerson() and we have defined a resolver.

Now we know the basics of creating a GraphQL server.

Summary

We’ve learned that there are two major ways to declare a schema:

the raw approach, more to type

the GraphQL schema definition approach using buildSchema()

We’ve also covered how we can answer a query by invoking resolvers, which are functions that needs to fetch the data from somewhere.

Lastly we’ve learnt how to create mutations.