The GraphQL Practices series contains the following articles, which will be updated in the near future:

  • GraphQL Practice part 1: Introduction to GraphQL

  • Use GraphQL in an Egg

  • How does GraphQL implement file upload

  • GraphQL subscription

  • GraphQL N + 1 problem

  • GraphQL best practices

  • This article code: github.com/nodejh/egg-…

preface

As a front-end, you must hate poorly written interface documentation, you must complain about redundant interface fields, and you must hate having to request multiple interfaces to get the data you want. As a back end, you must hate to document your interface, you must hate to complain that the front end doesn’t get the values it wants, and you must hate to have the front end tell your interface what to do.

How to resolve the differences between the front and rear ends? How to improve development efficiency? That’s GraphQL.

GraphQL is an API query language, in the same class of technologies as RESTful apis. In other words, GraphQL is an alternative to RESTful apis.

The following article takes a closer look at GraphQL practices based on Node.js.

Related technology stack:

  • The backend: Egg. Js
  • GraphQL Server: apollo-server
  • Egg GraphQL plugin: egg-plugin-graphQL
  • Front end: the react – Apollo

GraphQL profile

As you already know, egg.js applications generally follow the classic MVC pattern:

  • router(routing)
  • controller
  • service
  • public(view)

Get (‘/ API /user’, controller.user.index), and then implement controller logic in controller. These include parameter verification, permission control, calling service, and returning results. A service is a concrete implementation of a business, such as operating a database.

When GraphQL was used, router and Controller were used instead. Because GraphQL has implemented routing and parameter verification for us, and we can call services in GraphQL.

So how does GraphQL work? This leads to the two concepts of GraphQL: Schema and resolver.

Schema

Schema is a description of data in GraphQL, similar to TypeScript type definitions. Here’s an example:

enum Gender {
  MALE
  FEMALE
  NONE
}

type User {
  name: String!
  gender: Gender!
  tags: [String! ] ! }Copy the code

As shown above, Gender is an enumerated type whose values are MALE, FEMALE, and NONE.

User is a GraphQL object type with two fields, name and tags. Where name is of type String! ,! Indicates that the field is non-empty. [String!] ! Represents an array of strings with non-empty elements and non-empty tags.

There are two special types in schema: query and mutation. Each GraphQL service has a Query type and possibly a mutation type. Query is used to query data, and mutation is used to create or update data. These two types are the same as regular object types, but they are special because they define the entry point for each GraphQL query, which is the equivalent of a RESTful route.

Suppose I now need two interfaces, one to Query all users and the other to Query users by name, then I need a schema with a Query type with users and user fields on it:

type Query {
  "Query all user list"users: [User!] !"Query user information by name"
  user(name: String!). : User }Copy the code

As you can see from schema, GraphQL is strongly typed. The main types of GraphQL are object types, enumerations, scalar types, interfaces, unions, and input types.

Scalar types are leaf nodes of GraphQL queries that correspond to fields without any secondary fields. The default scalar types for GraphQL are:

  • Int: signed 32-bit integer.
  • Float: signed double precision floating point value.
  • String: UTF‐8 character sequence. Boolean: true or false.
  • ID: The ID scalar type represents a unique identifier, usually used to retrieve an object or as a key in the cache. The ID type is serialized in the same way as String; However, defining it as an ID means that it does not need to be human-readable. For example, the ID field of MongoDB.

We can also customize other scalars, such as Scalar Date, with the Scalar field.

A detailed description of the GraphQL types can be found in the documentation Schema and Types.

Resolver

Once the schema is defined, you need to implement Query or mutation in the schema. In GraphQL, each field of each type is supported by a resolver function. When a field is executed, the corresponding resolver is called to generate the next value.

To put it simply, each field in the schema needs to correspond to a resolver function, which is equivalent to controller in RESTful. The resolver function is the implementation of GraphQL entry.

For the previous Schema, we need to define the resolver function for the users and user fields in Query:

const resolvers = {
  Query: {
    users: (a)= > [{ name: "Jack".gender: "MALE".tags: ["Alibaba"]}],user: (parent, args, context, info) = > {
      const { name } = args;
      // find user by name...
      return { name, gender: "MALE".tags: ["Alibaba"]}},},};Copy the code

The resolver function also supports promises, such as:

const resolvers = {
  Query: {
    users: async (parent, info, context) => {
      return awaitcontext.service.user.findAll(); }}};Copy the code

The instance

To see how this works, we can start both the schema and the resolvers from apollo-server:

$ mkdir server
$ npm init -y
$ npm install apollo-server --save
Copy the code
// server/index.js
const { ApolloServer, gql } = require('apollo-server');

// The GraphQL schema
const typeDefs = gql` enum Gender { MALE FEMALE NONE } type User { name: String! gender: Gender! tags: [String!] ! } type Query {" Query all users "users: [User! ! "Query user information by name" user(name: String!) : User } `;

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    users: (a)= > [{ name: "Jack".gender: "MALE".tags: ["Alibaba"] {},name: 'Joe'.gender: 'MALE'.tags: []}],user: (parent, args, context, info) = > {
      const { name } = args;
      // find user by name...
      return { name, gender: "MALE".tags: [name] }
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) = > {
  console.log(` 🚀 Server ready at${url}`);
});
Copy the code

After node index.js is started, open the corresponding URL in the browser (default is http://localhost:4000), and you can see a powerful GraphQL developer tool (playground). We can query it in the left query statement. The corresponding data is displayed on the right.

Query query

Ask for the data you want, no more, no less

In RESTful apis, data is generally queried through interfaces. For example, to query the list of all users, HTTP GET request/API/Users interface may be used. In GraphQL, the concept of routing is replaced by entry. Similarly, we can query entry in GraphQL Schema through the query statement of GraphQL.

Search criteria:

query {
  users {
    name
  }
}
Copy the code

The meaning of this query is to query users and return the name field. The query results are as follows:

{
  "data": {
    "users": [{"name": "Jack"
      },
      {
        "name": "Joe"}}}]Copy the code

If we also need to get gender and tags, we can write:

query {
  users {
    name
    gender
    tags
  }
}
Copy the code

Query result:

{
  "data": {
    "users": [{"name": "Jack"."gender": "MALE"."tags": [
          "Alibaba"] {},"name": "Joe"."gender": "MALE"."tags": []}]}}Copy the code

In contrast to RESTful apis, GraphQL’s query statements have more descriptions of return fields. We can query what fields we need, which solves the problem of field redundancy. At the same time, GraphQL queries can always get predictable results, and the fields in the query results must be one-to-one corresponding to the query conditions.

Get multiple resources with a single request

Of course, GraphQL does more than that.

Imagine another scenario: query a list of all user names and return details for the user name “Jack”.

If using RESTful apis, we might need to make two HTTP requests: / API /users and/API /user? Name = Jack interface. With GraphQL, we only need to define one query condition:

query GetUser {
  users {
    name
  }
  user(name: "Jack") {
    name
    gender
    tags
  }
}
Copy the code

In this query, we queried two entries:

  • The queryusersTo return tonamefield
  • To query information, use {“name”: “Jack”} as parameteruserTo return toname,gendertagsfield

Using the browser developer tools to view the request after executing the query criteria, you can see that only one request was sent, with parameters operationName Query and variables.

  • operationNameIs the name of the operation we defined, i.eGetUserCan be omitted
  • queryYes Query condition
  • variablesVariables, which were not used in the query above, are now empty objects

After receiving the HTTP request, the GraphQL server will parse the Schema according to the query conditions, that is, execute the resolver function of the corresponding field in the Schema according to the query fields.

It is important to note that the query field is executed in parallel, while the mutation field is executed linearly, one after the other.

This is also the power of GraphQL. We only need to write the schema and resolver, and GraphQL will automatically implement the orchestration of the service according to the query statement. This also solves another problem with front-end collaboration: the need for the front end to aggregate multiple interfaces to get the desired data.

Change the mutation

This is all about fetching data, but any complete data platform also needs a way to alter server-side data.

In RESTful apis, any request can end up causing some server-side side effects, but convention recommends against using GET requests to modify data. GraphQL is similar. Technically, any query can be implemented to cause data writes, but GraphQL establishes a specification that any modification of data should be sent using mutation.

For example, to create a user, the schema should be defined using Mutation:

input UserInput {
  name: String! gender: Gender! } type Mutation { createUser(user: UserInput!) : User! createUserTag(tag:String!). : User! }Copy the code

Input represents an input object, which looks exactly like an ordinary object except that the keyword is input instead of type. It is special in that the input object can be used in complex parameters, often mutation parameters, such as the createUser parameter above.

After Mutation is defined, the corresponding resolver function should also be defined:

const resolvers = {
  Query: {
    // ...
  },
  Mutation: {
    createUser: (parent, args, context, info) = > {
      const { user: { name, gender } } = args;
      // insert user to db...
      return { name, gender, tags: []}; },createUserTag: (parent, args, context, info) = > {
      const { tag } = args;
      return { name: "Jack".gender: "MALE".tags: [ tag ] }
    },
  },
};
Copy the code

So we can request to create the user’s GraphQL interface like this:

mutation CreateUser {
  createUser(user: { "name": "Jack"."gender": "MALE" }) {
    name
    gender
  }
}
Copy the code

Or use variables:

mutation CreateUser($user: UserInput!) {
  createUser(user: $user) {
    name
    gender
  }
}
Copy the code

In developer tools, you can add VARIABLES to the QUERY VARIABLES panel in the lower left corner:

For more information about queries and changes, see the GraphQL documentation: Queries and Changes.

The front-end development

So far, we’ve built a simple server and interface using GraphQL and made queries and changes in Playground. So how do you use the GraphQL interface in the front end?

We’ve already seen that each execution of GraphQL is actually an HTTP request to the server. However, there are many open source GraphQL clients that simplify our work. The most recommended one is Apollo Client, which supports React/Vue/Angular frameworks.

Initialize the project

Using react.js as an example, we first initialize a project:

$ npx create-react-app client
Copy the code

Then install the GraphQL Client dependencies:

$ cd client
$ yarn add apollo-boost @apollo/react-hooks graphql
Copy the code

Because create-react-app uses YARN by default, we install dependency packages using YARN in the front end. NPM install Apollo-boost @apollo/react-hooks graphQL –save

Create a Client

After installing the dependencies, you can create a GraphQL Client that requests data from the GraphQL server.

Modify SRC /index.js by referring to the following code, where uri is the address of our GraphQL server, ApolloProvider stores all GraphQL data, so it is recommended to use it as the root component of the application.

// ...
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from '@apollo/react-hooks';
// ...

const client = new ApolloClient({
  uri: 'http://localhost:4000'});const Root = (a)= > (
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

ReactDOM.render(<Root />, document.getElementById('root'));
Copy the code

To get the data

Usually for a React project, we need to use Redux to get data, and we need to write various actions and dispatches, which is very tedious. But now with GraphQL, everything is much simpler!

Apollo /react-hooks @Apollo/React-hooks provides hooks like useQuery, useMutation, etc.

import React from 'react';
import gql from 'graphql-tag';
import { useQuery } from '@apollo/react-hooks';

// Define the query statement
const GET_USERS = gql` query { users { name } } `;

function Users() {
  // Use useQuery hook to retrieve data
  const { loading, error, data } = useQuery(GET_USERS);

  if (loading) return 'Loading... ';
  if (error) return `Error! ${error.message}`;

  return (
    <div>
      {data.users.map(user => <p>{user.name}</p>)}
    </div>
  );
}

export default Users;
Copy the code

First we define a query statement, just like we defined in Playground, except that we need to put it in GQL. Then use useQuery to obtain data, useQuery returns loading error data and other properties, we can easily handle the loading state of the page and interface failure.

As mentioned above, GraphQL’s query is predictable. According to the query conditions, it is also clear that the data attribute must have a Users field, and each user must have only a name field.

Note that user.name may be null if name is allowed to be null in the server’s GraphQL Schema. If the name cannot be null, the server will report an error when user.name is null.

Start the front-end by using YARN Start, and the following page is displayed in the browser:

Query with parameters

The query with parameters is also very simple, we just need to define the parameter variable names in the query statement and pass the variables in useQuery’s variables.

In the following code, the variable name for the query GET_USER is $userName of type String and cannot be null. To pass variable values through variables, we only need to use userName.

import React from 'react';
import gql from 'graphql-tag';
import { useQuery } from '@apollo/react-hooks';

// Define the query statement
const GET_USER = gql` query GET_USER($userName: String!) { user(name: $userName) { name gender tags } } `;

function User() {
  // Use useQuery hook to retrieve data
  const { loading, error, data } = useQuery(GET_USER, {
    variables: {
      userName: 'Jack',}});if (loading) return 'Loading... ';
  if (error) return `Error! ${error.message}`;

  return (
    <div>
      <p>Name: {data.user.name}</p>
      <p>Gender: {data.user.gender}</p>
      <p>Tags: {data.user.tags.join(',')}</p>
    </div>
  );
}

export default User;
Copy the code

conclusion

As you can see, GraphQL is very simple to write, both server-side and front-end, because GraphQL and the tools in its ecosystem help us do a lot of work and save a lot of development costs.

When developing the service side, we can make the service more atomic. We don’t care what fields the front end needs. We can write the service according to the GraphQL Schema based on the best practices of the back end. The front end can compose data and services according to the Schema without constantly asking the back end to add or subtract fields or interfaces.

In the development of front-end, it is even simpler. No longer need to repeatedly write various actions, directly use hooks to achieve data acquisition and update, and also very simple to know the loading and abnormal state, simple, direct and efficient.

GraphQL Playground provides debugging and documentation services. We can easily debug the GraphQL interface in Playground, with all sorts of syntax hints, without having to use HTTP tools like Postman to request the interface. At the same time, the playground also comes with a document function, which is automatically generated based on GraphQL Schema. In this way, the documents can be updated in real time with the code. Developers no longer need to spend time writing documents, and users can always see the latest and most accurate documents.

Overall, using GraphQL is a great way to improve development efficiency.