This content is based on the text version shared before. If you want to see the key points, you can see the PPT before.
What is a GraphQL
GraphQL is a data query language developed by Facebook. If you have written SQL query language, you can think of it as SQL query language, but GraphQL is for clients to query data. While this may sound like database software to you, GraphQL is not database software. You can think of GraphQL as middleware, a bridge between a client and a database. The client gives it a description and then returns data from the database that matches that description. This also means that GraphQL doesn’t care what database the data is in.
GraphQL is also a set of standards under which different platforms and languages have corresponding implementations. GraphQL also has a type system designed to provide a relatively safe development experience similar to TypeScript within the constraints of the type system.
What problem does GraphQL solve
Let’s start by reviewing RESTful API design with which we are already familiar. Simply put, RESTful apis use urls to express and locate resources, and USE HTTP verbs to describe operations on resources.
Let’s use the IMDB movie info details page as an example to see what apis we need to satisfy RESTful API design requirements. Let’s take a look at what information is needed on the home page.
As you can see, the page is made up of basic movie information, actor and rating/review information. According to the design requirements, we need to put these three resources under different apis. First, the basic movie information, we have API /movie/:id, given a movie ID returns the basic information data.
Pretend to GET data in JSON format:
{name: "Manchester by the Sea", ratings: "PG-13", Score: 8.2, release: "2016", actors: [https://api/movie/1/actor/1/], reviews: [https://api/movie/1/reviews]}Copy the code
This contains the information we need for the movie’s name, rating, and so on, as well as a piece of data called HyperMedia, which is usually a URL that specifies the API endpoint address where the resource can be retrieved. If we follow the connection request that HyperMedia points to, we can get all the information we need on our page.
GET /api/movue/1/actor/1
{
name: “Ben Affleck”,
dob: “1971-01-26”,
desc: “blablabla”,
movies:[“https://api/movie/1”]
}
Copy the code
GET /api/movie/1/reviews
[{content: "Its as good as...", Score: 9}]Copy the code
Finally, we need to request all THE API endpoints that contain the required information. For mobile terminals, it is relatively expensive to initiate an HTTP request, especially in the case of poor network connection quality, sending multiple requests at once will lead to a bad experience.
And in such AN API design, specific resources are distributed among specific API endpoints, which is convenient to write on the back end, but not necessarily on the Web or client side. For example, on an Android or iOS client, if a release updates a feature that is very explosive, the same API may spit out more data to support this feature. However, for unupgraded clients, this new data is meaningless and causes a certain amount of resource waste. If all resources are consolidated into one API, it is also possible to increase the amount of data by integrating irrelevant data.
GraphQL is designed to solve these problems by sending a description to the server to inform the client of all the data required. Data control can even be refined down to fields, so as to achieve the purpose of obtaining all the required data in a single request.
GraphQL Hello World
GraphQL request body
Let’s take a look at what a GraphQL request looks like:
query myQry ($name: String!) {movie(name: "Manchester") {name desc ratings}}Copy the code
Does this request structure look anything like JSON? This is a deliberate design by Facebook, and hopefully after you’ve read this, you’ll appreciate the good intentions of Facebook.
So, the request description above is called a GraphQL request body, which describes what data you want to fetch from the server. The general request body consists of several parts, from the inside out.
The first is the field, which requests a unit of data. Scalar fields are also the most fine-grained data units in GraphQL, and are the last field in the returned JSON response data. That is, if it is an Object, at least one of the fields must be selected.
Putting together the fields we need, we call it the selection set of something. The above name, desc and ratings together are called the selection set of movie. Similarly, movie is the selection set of myQry. It is important to note that the selection set operation cannot be used on scalars because it is the last layer.
Next to movie, name: “Manchester”, this represents the parameter passed in to movie. The parameter name is named and the value is Manchester. Use these parameters to tell the server what conditions you need the data to meet.
Finally, we come to the outermost layer of the request body:
- Operation type: Specifies what the request body will do to the data, similar to GET and POST in REST. The basic operation types in GraphQL are
query
Represents a query,mutation
Operations are performed on data, such as adding, deleting, or modifying data,subscription
Subscribe operation. - Operation name: The operation name is an optional parameter. The operation name does not affect the entire request, but only gives the request body a name, which can be used as a basis for debugging.
- Variable definition: In GraphQL, declare a variable to use
$
The colon is followed by the incoming type of the variable. If you want to use a variable, just reference it; for example, movie above can be rewritten tomovie(name: $name)
.
If none of the above is provided, the request body is treated as a Query operation by default.
Result of request
If we execute the request body above, we should get the following data:
{
"data": {
"movie": {
"name": "Manchester By the Sea"."desc": "A depressed uncle is asked to take care of his teenage nephew after the boy's father dies. "."ratings": "R"}}}Copy the code
If you carefully compare the structure of the result with that of the request body, you will find that it is exactly the same as the structure of the request body. That is, the structure of the request body also determines the structure of the data that will eventually be returned.
GraphQL Server
In the previous REST example, we requested multiple resources with multiple API endpoints. In GraphQL, there is only one API endpoint, which also accepts GET and POST verbs and uses POST requests to manipulate mutation.
GraphQL is a standard. There are libraries to parse it. For example, Facebook’s official graphqL.js. Apollo, developed by the Meteor team, developed both clients and servers, as well as supporting the popular Vue and React frameworks. On the debugging side, you can use Graphiql for debugging, and thanks to GraphQL’s type system and Schema, you can also use auto-complete in Graphiql debugging.
Schema
As mentioned earlier, GraphQL has a type system, so what are the conventions for each field type? The answer is in this section. In GraphQL, the type definition and the query itself are defined by Schema. The full name of GraphQL’s Schema Language is Schema Definition Language. The Schema itself does not represent the actual data structure in your database. Its definition determines what this entire endpoint can do, what we can ask of it, what we can do with it. Recalling the previous request body, the request body determines the structure of the returned data, while the definition of the Schema determines the capabilities of the endpoint.
Let’s take a look at schemas one by one.
Type systems, scalar types, non-null types, parameters
First look at the Schema on the right: Type is the most basic concept in GraphQL Schema. It represents a GraphQL object type, which can be easily understood as an object in JavaScript. In JavaScript, an object can contain various keys. A type can also contain a variety of fields, and the field types can be not only scalar types, but also other types defined in the Schema. For example, in the Schema above, the type of the Movie field under Query could be Movie.
In GraphQL, there are several scalar types: Int, Float, String, Boolean, and ID, representing integer, Float, String, Boolean, and an ID, respectively. The ID type represents a unique identifier. The ID type is eventually converted to String, but it must be unique. For example, the _ID field in mongodb can be set to ID. Meanwhile, these scalar types can be understood as primitive types in JavaScript. The above scalar types can also correspond to Number, Number, String, Boolean, and Symbol in JavaScript.
Another point to note here is the Type Query, which is the entry point to all Query queries in the Schema, as well as Mutation and Subscription, as the entry point for the corresponding operation.
In the Movie field under Type Query, we use parentheses to define the parameter names and the types of parameters we can accept. In the Schema above, the exclamation mark immediately followed by an exclamation mark declares that this type is a non-nullable type. Declaring the parameter in a parameter means that it cannot be passed null. If the exclamation mark follows the field, the field must not be empty when the type of data is returned.
From the above type definition, you can see that the type system in GraphQL plays an important role. In this case, the Schema defines name as a String, so you cannot pass in an Int, which will throw a type mismatch error. Similarly, if the incoming ratings data type is not String, a type mismatch error will also be thrown.
List, Enumeration type
If we have a field that returns more than one scalar type of data, but rather a set of data, we need to declare the List type, surrounded by brackets [] around the scalar type, the same way arrays are written in JavaScript, and the returned data will also be array types.
Note that [Movie]! With [Movie!] The meaning is different: the former means that movies fields always return non-null but the Movie element can be null. The latter means that Movie elements returned from Movies cannot be empty, but movies fields can be.
You may notice in the request body that the value of the genre parameter is not enclosed in double quotes, nor is it any built-in type. As you can see from the Schema definition, COMEDY is an enumerator in the enumeration type MovieTypes. Enumeration types are used to declare a list of value constants. If a parameter is declared as an enumeration type, it can only be passed the name of the constant qualified within that enumeration type.
Passing complex structure parameters (Input)
In the previous example, the parameters passed in were scalar types, so what if we wanted to pass in data with a complex structure? The answer is to use keyword input. It is used in exactly the same way as type.
According to the Schema definition in this example, the parameter of data when we query search must be
{ term: "Deepwater Horizon" }
Copy the code
Aliases
Imagine a page where I want to list information about two movies for comparison. To take advantage of GraphQL, I want to query information about both movies at the same time, requesting movie data in the request body. As mentioned earlier, the body of the request determines the structure of the returned data. Two data with movie keys were detected before data return, and only one data could be obtained due to duplicate keys after merging. So in this case we need to use the alias function.
Alias is another name for the returned field. It is easy to use alias: before the field in the request body. The returned data will be automatically replaced with that name.
Fragment Spread Fragment Spread Fragment Spread
In the example above, we need to compare the data for the two movies. If it is a hardware comparison site, the number of hardware queries is often more than two. Writing redundant selection sets at this point is cumbersome, bloated, and difficult to maintain. To solve this problem, we can use fragment functionality. GraphQL allows you to define a common selection set, called a fragment. The fragment name on Type syntax is used to define fragments, where name is the name of the user-defined fragment and Type is the Type from which the fragment comes.
In this example, the common part of the selection set of the request body after extracting into segments is
fragment movieInfo on Movie {
name
desc
}
Copy the code
Before we can actually use fragments, we need to introduce you to the fragment deconstruction feature. Javascript-like structure. GraphQL’s fragment structure symbol “structures” the fields within the fragment into the selection set.
Interface
Like most other languages, GraphQL provides the ability to define interfaces. An interface refers to a collection of fields provided by the GraphQL entity type itself, defining a set of ways to communicate with the outside world. A type that uses IMPLEMENTS must contain the fields defined in the interface.
interface Basic { name: String! year: Number! } type Song implements Basic { name: String! year: Number! artist: [String]! } type Video implements Basic { name: String! year: Number! performers: [String]! } Query { search(term: String!) : [Basic]! }Copy the code
In this example, a Basic interface is defined, and both Song and Video types implement fields for that interface. The interface is then returned in the search query.
The searchMedia query returns a set of Basic interfaces. Because fields in the interface are common to all types that implement the interface, they can be used directly on the request body. For performers in Video, it may be problematic to directly select other non-common fields on a specific type, for example, it may be all types of data returned by searchMedia that realize the interface, but it can not be performers in Song. At this point we can use the help of inline fragments (described below).
Union type
Union types are much the same as interface concepts, except that there are no common fields defined between types under union types. Inline fragment must be used in the Union type for the same reason as the interface type above.
union SearchResult = Song | Video Query { search(term: String!) : [SearchResult]! }Copy the code
Inline Fragment
When querying the interface or union type, the selected fields may be different due to different return types. In this case, you need to use inline fragments to determine the selection set for a specific type. The concept and usage of an inline selection set are basically the same as that of an ordinary fragment. The difference is that the inline fragment is declared directly in the selection set and does not need to be declared in the fragment.
Example of a query interface:
query {
searchMedia(term: "AJR") { name year ... on Song { artist } ... on Video { performers } } }Copy the code
First, we need two public fields on the interface, and choose Artist field when the result is Song, and Video field when the result is Video. The same is true for the query union type example below.
Examples of querying union types:
query {
searchStats(player: "Aaron") {... on NFLScore { YDS TD } ... on MLBScore { ERA IP } } }Copy the code
GraphQL built-in instructions
GraphQL has two built-in logical instructions that are used after the field name.
@include
When the condition is true, query this field
query {
search {
actors @include(if: $queryActor) {
name
}
}
}
Copy the code
@skip
When the condition is true, this field is not queried
query {
search {
comments @skip(if: $noComments) {
from
}
}
}
Copy the code
Resolvers
Now that we know about the request body and the Schema, where does our data come from? The answer comes from the Resolver function.
The concept of Resolver is very simple. When the request body queries a field, the corresponding Resolver function will be executed. The Resolver function is responsible for retrieving data from the database and returning the specified field in the request body.
type Movie {
name
genre
}
type Query {
movie: Movie!
}
Copy the code
When the request body queries for movie, the Resolver with the same name must return data of type movie. You can also use a separate Resolver for the name field. The Resolver will be clear in the following code examples.
Build GraphQL API with ThinkJS
ThinkJS is a Future-oriented Node.js framework that incorporates a host of project best practices to make enterprise development easy and efficient. The framework is based on Koa 2.x, which is compatible with all functions of Koa.
In this example, we will use ThinkJS and MongoDB to build GraphQL API. The simplicity of ThinksJS will make you love it!
Fast installation
NPM install -g think-cli
Use CLI to quickly create project ThinkJS New GQLDemo
Go to NPM install && NPM start in the project directory
ThinkJS server was built in less than two minutes, so easy!
Configuring the MongoDB Database
Since I prefer Mongoose, it just so happens that ThinkJS officially provides think-Mongoose library for quick use. After installation, we need to introduce and load the plug-in in SRC /config/extend.js.
const mongoose = require('think-mongoose');
module.exports = [mongoose(think.app)];
Copy the code
Next, configure the database connection in Adapter.js
export.model = {
type: 'mongoose'.mongoose: {
connectionString: 'mongodb:// your database/GQL '.options: {}}};Copy the code
Now that we have mongoose instances in our entire ThinkJS application, what else is missing? Data model!
With the powerful data model function of ThinkJS, we only need to take the name of the data set as the file name and define the model to use, which is simpler than the original operation of Mongoose.
In this example, we implement actor and movie, respectively create actor.js and movie.js in the model directory, and define the model inside.
actor.js
module.exports = class extends think.Mongoose {
get schema() {
return {
name: String.desc: String.dob: String.photo: String.addr: String.movies: [{type: think.Mongoose.Schema.Types.ObjectId,
ref: 'movie'}}; }};Copy the code
movie.js
module.exports = class extends think.Mongoose {
get schema() {
return {
name: String.desc: String.ratings: String.score: Number.release: String.cover: String.actors: [{type: think.Mongoose.Schema.Types.ObjectId,
ref: 'actor'}}; }};Copy the code
Middleware that processes GraphQL requests
To process GraphQL requests, we have to intercept specific requests for parsing. In ThinkJS, we can fully leverage the capabilities of middleware for parsing and data return. Middleware is configured in middleware.js.
There are three key parameters for configuring middleware in ThinkJS:
- match: is used to match the URL to which we want our request sent
/graphql
Then we match this path and process it; - Handle: the processing function of middleware. When match arrives, this processing function will be called and executed. Our parsing task will also be carried out here and the parsing result will be returned.
- Options: Parameters passed to the middleware in options, where we can pass our Schema, etc., to the parser.
Our middleware configuration looks something like this:
{
match: '/graphql'.handle: (a)= > {},
options: {}}Copy the code
Parse the core of GraphQL
Apollo Server
Apollo Server is a GraphQL service middleware built on node.js. Its strong compatibility and excellent stability are the primary reasons for choosing this middleware in this paper.
Although There is no ThinkJS version of Apollo Server middleware, this can be resolved using runHttpQuery, the Core method in Apollo Server Core.
Install it into our project: NPM install Apollo-server-core GraphQL –save
Writing middleware
RunHttpQuery takes two parameters. The first parameter is GraphQLServerOptions, which we can leave empty. The second is the HttpQueryRequest object. We need at least methods, Options, and Query,
They represent the method of the current request, the GraphQL service configuration, and the request body, respectively.
The schema should be a GraphQLSchema instance. For the schema Language written directly in our previous example, it is not recognized. At this point, we need to use the makeExecutableSchema tool in GraphQL-Tools to associate our Schema and Resolvers into GraphQLSchema instances.
Install it into our project: NPM install GraphQL-tools –save
Write a Schema and Resolver
Before converting to GraphQLSchema, we need to get our Schema and Resolver ready.
Using the knowledge we have learned, we can quickly write a simple Schema to provide an interface for querying actor information and movie information.
type Movie {
name: String!
desc: String!
ratings: String!
score: Float!
cover: String!
actors: [Actor]
}
type Actor {
name: String!
desc: String!
dob: String!
photo: String!
movies: [Movie]
}
typeQuery { movie(name: String!) : [Movie] actor(name: String!) : [Actor] }Copy the code
Next, write the Resolver function that parses the movie and Actor fields under Query, respectively.
const MovieModel = think.mongoose('movie');
const ActorModel = think.mongoose('actor');
module.exports = {
Query: {
movie(prev, args, context) {
return MovieModel.find({ name: args.name })
.sort({ _id: - 1 })
.exec();
},
actor(prev, args, context) {
return ActorModel.find({ name: args.name })
.sort({ _id: - 1}) .exec(); }}}Copy the code
To properly associate the Schema, the structure of the Resolver function must be consistent with that of the Schema.
Did you find anything wrong when you got to this point?
From the previous data model definition, movies and Actors fields are references to data from one set of movies and actors from another set. The purpose of the Resolver is to establish and maintain the relationship between movies and actors. The GraphQL throws an error if it does not fit the Schema definition.
So how can this problem be solved? As mentioned in the previous section, each field can have a corresponding Resolver function. We set Resolver functions for movies and Actors fields, and query the ids resolved by the last Resolver. Finally, the returned data conforms to the Schema definition.
const MovieModel = think.mongoose('movie');
const ActorModel = think.mongoose('actor');
module.exports = {
Query: {
movie(prev, args, context) {
return MovieModel.find({ name: args.name })
.sort({ _id: - 1 })
.exec();
},
actor(prev, args, context) {
return ActorModel.find({ name: args.name })
.sort({ _id: - 1}) .exec(); }},Actor: {
movies(prev, args, context) {
return Promise.all(
prev.movies.map(_id= >MovieModel.findOne({ _id }).exec()) ); }},Movie: {
actors(prev, args, context) {
return Promise.all(
prev.actors.map(_id= >ActorModel.findOne({ _id }).exec()) ); }}}Copy the code
The prev parameter used is the data parsed by the previous Resolver.
Combined into a GraphQLSchema instance
Now that we have the Schema and Resolver, we can finally turn them into a GraphQLSchema instance.
Call makeEcecutableSchema in GraphQL-Tools and put it in options for later use.
Now our middle looks like this:
const { makeExecutableSchema } = require('graphql-tools');
const Resolvers = require('./resolvers'); // We just wrote Resolver
const Schema = require('./schema'); // The Schema we just wrote
module.exports = {
match: '/graphql'.handle: (a)= > {},
options: {
schema: makeExecutableSchema({
typeDefs: Schema,
resolvers: Resolvers
})
}
}
Copy the code
Write a handler
Welcome runHttpQuery from Apollo-server-core!
const { runHttpQuery } = require('apollo-server-core');
Copy the code
Referring to Apollo-server-KOA, a ThinkJS version of Apollo-server middleware was quickly built.
const { runHttpQuery } = require('apollo-server-core');
module.exports = (options = {}) = > {
return ctx= > {
return runHttpQuery([ctx], {
method: ctx.request.method,
options,
query:
ctx.request.method === 'POST'
? ctx.post()
: ctx.param()
}).then(
rsp= > {
ctx.set('Content-Type'.'application/json');
ctx.body = rsp;
},
err => {
if(err.name ! = ='HttpQueryError') throw err;
err.headers &&
Object.keys(err.headers).forEach(header= >{ ctx.set(header, err.headers[header]); }); ctx.status = err.statusCode; ctx.body = err.message; }); }; };Copy the code
After NPM start is up and running, use GraphiQL to “play” your request body (remember to fill data into database first).
GraphQL’s pros and cons
advantages
- What you see is what you get: The written request body is the final data structure
- Reduce network requests: complex data retrieval can also be done in one request
- Schema is document: The Schema defined also specifies the rules for requests
- Type checking: Rigorous type checking can eliminate certain perceived errors
disadvantages
- Increased complexity of server implementation: Some businesses may not be able to migrate using GraphQL, although the original business requests can be brokered using middleware, which will undoubtedly increase complexity and resource consumption
The full source code can be found here, and the middleware can be found here