Author’s brief introduction

Gu Yingjie is senior manager of RESEARCH and development at Ctrip, responsible for the design, development and maintenance of front-end frameworks and infrastructure. Author of the open source projects React – Lite and React – IMVC.

preface

With the development of multi-terminal, multi-platform, multi-service form, multi-technology selection and other aspects, the data interaction between the front and back ends becomes increasingly complex.

The same data may be consumed in many different forms and structures under many scenarios.

Ideally, all of this complexity can be taken on by the back end. The front end just gets the integrated data from the back end interface.

However, whether it’s because of the back-end domain model or because of the microservices architecture. As the front end, what we feel is that the back end provides interfaces that are less and less front-end friendly. We have to compose multiple back-end interfaces ourselves to get a complete data structure.

There is an irreconcilable contradiction between the back-end requirements of domain-oriented model and the front-end requirements of page-oriented presentation.

We tried to solve this problem with a RESTful style wireless API aggregation layer. This layer is completely developed and iterated by back-end engineers (the front end acts as a contract for the downstream waiting aggregation interface), who understand both the microservice architecture behind the docking and the presentation requirements of each front-end page.

As the number of pages increases and the number of interface callers increases, the logic of the back-end API aggregation layer becomes heavier and heavier. The aggregation layer itself becomes the new bottleneck. In fact, as the back-end of the middle layer, it is difficult to fully connect with the micro-service system behind it, and it is difficult to fully understand the page presentation requirements of the front end.

Their overall positioning is awkward, with both UI presentation related information and a lot of interface related business logic processing in the code. With each release iteration, there is a lot of communication and tuning downstream.

We eventually realized that it might be better to let the front end, which knows the page data logic best, take over the middle tier and let the back end go back to their domain model. It can reduce the communication cost, improve the professional degree of the front and back end, reduce the pressure of joint investigation, and speed up the development efficiency of each other.

In this context, we finally choose to use Node.js to build a Backend dedicated to front-end page rendering, also known as backend-for-frontend (BFF For short).

We were faced with a number of different technology choices, which revolved around the tradeoff between RESTful apis and GraphQL.

As the title suggests, we ended up with GraphQL as BFF.

This article will introduce our investigation, exploration, tradeoff, technology selection and design of GraphQL, hoping to give you some inspiration.

First, the emergence of GraphQL pattern is inevitable

The front-end page-oriented data aggregation layer, whose interface is easy to become more complex in the iterative process; The result is a superinterface.

It has many callers, different invocation scenarios, and even multiple versions of interfaces that provide data services.

All of this complexity is reflected in the interface parameters.

The more scenarios an interface is called in, the higher its requirement on the expression ability of the interface parameter structure. If you only have a Boolean parameter of type, can only meet true | false two scenarios.

Taking the product detail interface as an example, a natural request parameter structure is as follows:

It contains ChannelCode channel information, IsOp identity information, MarketingInfo marketing information, PlatformId platform information, QueryNode QueryNode information, and Version Version information. The most core parameter, ProductId, is surrounded by a large number of scenario-related parameters.

If you take a look at the QueryNode parameter, you can easily see that it is the prototype of GraphQL. It’s just that it uses more complex JSON to describe query fields, whereas GraphQL does the same thing with more concise query statements.

Also, QueryNode parameters support only one level of field filtering; GraphQL supports multilevel filtering.

GraphQL can be thought of as a specialization of parameter design in the form of QueryNode. Instead of describing query results using JSON, GraphQL designs a more complete DSL, integrating fields, structures, parameters, and so on.

Following Greenspan’s tenth Law:

Any C or Fortran program complex enough will contain an AD hoc, substandard, bug-ridden, slow, half-functioning implementation of Common Lisp. https://zh.wikipedia.org/wiki/%E6%A0%BC%E6%9E%97%E6%96%AF%E6%BD%98%E7%AC%AC%E5%8D%81%E5%AE%9A%E5%BE%8B

It might be said:

Any interface design complex enough to include an improvised, substandard, half-functional implementation of GraphQL.

From SearchParams, FormData, JSON, and GraphQL query statements, we see new ways to communicate data that meet different scenarios and complexity requirements.

From this perspective, the emergence of GraphQL mode has a certain inevitability.

Ii. Inevitability in GraphQL language design

As a query-related DSL, the language design of GraphQL is also not arbitrary.

We can do a thought experiment.

Suppose you are an architect and you are given the task of designing a front-end friendly query language. Requirements:

1) Query syntax is similar to query results

2) Can accurately query the desired field

3) Can merge multiple requests into a query statement

4) There is no interface version management problem

5) Code is documentation

We know that the query results are in JSON data format. JSON is a key-value pair-style representation of data, so you can deduce the query statement backwards from the result.

Above is a query result. Obviously, its query cannot contain a value part. When we delete value, it looks like this.

The query statement has the same key and hierarchical relationship as the query result. That’s what we want.

We can go one step further and delete the redundant double quotes, commas, etc.

We have a simplified version that is already a valid GraphQL query.

The design ideas and processes are so simple and straightforward that it’s hard to imagine a more satisfying solution than the current one.

Of course, fields and hierarchies alone are not enough. There is too much data that fits this structure to query the entire database. We also need to design the way parameters are passed so that we can narrow down the range of data.

The picture above is a natural progression. Function calls are represented by parentheses, and arguments can be added, which is a classic design.

It is also highly similar to Method Definitions Sleep as described in ES2015. As follows:

The GraphQL parameter shown above is written with the literal userId: 123. This is not a particularly safe practice, as the developer injects literal values into the query by concatenating strings in the code, giving malicious attackers the opportunity to inject code.

We need to design a parameter variable syntax that specifies the location and number of parameters.

We can use the common notation $XXX, which is used in many languages to represent variables. Following this style can greatly reduce the cost of learning for developers.

Another pain point in front and back communication is naming. The front end often jokes that the field names on the back end are too long, or don’t make sense, or are misspelled, or don’t fit the front end’s conventions. Most commonly, back-end field names start with a capital letter, while front-end conventions like Class or Component start with a capital letter, and instances and data start with a lowercase letter.

We look forward to the opportunity to adjust field names.

The Alias mapping (Alias) syntax appears for this purpose.

This alias mapping syntax is common in other languages as well. If you don’t write it this way, the most you can do is:

Uid as uid or uid = uid makes little difference. I prefer the colon, which is close to the deconstruction syntax of ES2015.

At this point, we have the key hierarchy, parameter passing, variable writing, alias mapping, and so on, and we can write a sufficiently complex query. However, there are a few minor shortcomings.

For example, conditional expressions of fields. Suppose you have two queries. The only difference is that one has an A field and the other does not. The other fields and their structure are the same. Does the front end write two queries for such a small difference?

This is obviously not realistic, we need to design a syntax description and solve this problem.

It’s called a Directive.

Directives that can make additional statements about fields, such as

@include, whether to include the field;

@skip, whether this field is not included;

@deprecate, whether to deprecate this field;

In addition to the above default instructions, we can also support custom instructions and other functions.

The syntactic design of instructions can also be found in other languages. Java, Phthon, and ESNext all use the @ symbol to represent annotations, decorators, and other features.

With instructions, we can merge two highly similar query statements together and then switch them with conditional arguments. That’s a good idea. However, the instruction follows a single field and does not solve the multi-field problem.

For example, field A and field B have the same overall structure, with only one field name difference. The front end doesn’t want to write the same key multiple times.

This means that we need to design a Fragment grammar.

As shown above, declare a fragment with fragment and expand the fragment in an object field with three dots. We can write a common structure once and easily reuse it across multiple object fields.

This design is also a classic, similar to Spread Properties in JavaScript.

At this point, we have a relatively complete, front-end friendly query language design. It’s pretty much GraphQL in its current form.

As you can see, GraphQL’s query language design borrows from many mature designs in major development languages. It makes GraphQL easy for anyone with extensive programming experience to get started.

According to the same requirements, to do it again, the probability is high with the current form of the design. This is the inevitability of GraphQL language design as I understand it.

Composition and link of GraphQL

The query syntax is the front-end, or data-consuming, part of GraphQL.

In addition, GraphQL also provides a back-end oriented, or data-provider oriented, section. It is the Schema built based on GraphQL’s Type System.

A GraphQL service and query link, roughly as follows:

First, the server writes data types and builds a network of associations between data structures. The Query object is the entry point for data consumption. All queries are queries on fields in a Query object. You can think of the fields under Query as RESTful apis. For example, the query. post and query. author interfaces in the figure above are equivalent to /post and /author interfaces.

GraphQL Schema describes the type and structure of data, but it’s just shapes. It doesn’t contain real data. We need to write the Resolver function to get the real data.

The simple form of a Resolver is as follows

Each Query field has a value function that retrieves the parameters contained in the Query statement passed from the front end and then retrieves the data in any way. Resolver functions can be asynchronous.

With Resolver and Schema, we define both the shape of the data and how to get it. You can build a complete GraphQL service.

But they are just type and function definitions, and there is no real data interaction if the function is not called.

The query statement passed by the front end is the source that triggers the Resolver call.

 

As shown above, we initiate the query and pass the parameters. GraphQL parses our query and verifies the data shape with Schema to ensure that the structure of our query exists, that the parameters are sufficient, and that the types are consistent. Any link that has a problem will return an error message.

After the data shape verification is passed, GraphQL will trigger the corresponding Resolver function one by one according to the field structure contained in the Query statement to obtain the query result. In other words, if the front end does not query a field, the Resolver function corresponding to the field will not be triggered, and the data acquisition behavior will not occur.

In addition, if the Resolver returns data larger than the structure described in the Schema; The extra part will be ignored and not passed to the front end. This is a reasonable design. By controlling the Schema, we can control the data access rights of the front-end and prevent accidental disclosure of user accounts and passwords.

As such, the GraphQL service is able to fetch data on demand and deliver data precisely.

Clear up a few myths about GraphQL

There are quite a few developers out there who have all sorts of misconceptions about GraphQL. Here are a few important examples to clarify and help you understand GraphQL more fully.

4.1 GraphQL does not necessarily operate on a database

Some developers argue that GraphQL needs to operate on a database, so implementing it is almost like overthrowing the entire architecture of the current backend. This is a big misunderstanding.

Not only does GraphQL not operate on the database, it can even write data to Resolver without fetching it from anywhere else. Check out the official graphqL.js documentation and you can easily find examples:

The resolver function returns a Hello World String, ignoring all parameters.

As you can see, GraphQL is just a pair of calls to the Schema and resolver. It doesn’t make any assumptions about how to get data, where to get it, etc.

4.2 GraphQL is not antithetical to RESTful apis

There are quite a few GraphQL articles on the web that contrast it with RESTful apis, as if it were all GraphQL or all RESTful apis. This is also a major misconception.

Not only are GraphQL and RESTful apis not antagonistic, but they work together.

As you can see from the previous picture about the Resolver function, we can call the RESTful API in the Resolver function of GraphQL Schema to fetch data.

Of course, you can also call RPC or ORM to get data from other data interfaces or databases.

Therefore, implementing a GraphQL service does not need to challenge the current entire backend architecture. It is highly adaptable and can be embedded in the current system with low invasiveness.

4.3 GraphQL is not necessarily a back-end service

Most GraphQL, though, exists as a server. However, GraphQL as a language is not limited to back-end scenarios.

The bottom line is a normal function call that initiates a graphQL query. The response is as follows:

IO /s/hidden-wa: codesandbox.io/s/hidden-wa… View the results)

Therefore, we can use GraphQL purely on the front end to implement State Management. Frameworks like Relay include graphQL for use in the front end.

4.4 GraphQL does not necessarily require a Schema

It’s an interesting fact that GraphQL has two components in its design:

1) Data provider writes GraphQL Schema;

2) Data consumers write GraphQL Query;

This combination is officially best practice. But it is not a minimum configuration in a practical sense.

GraphQL Type System is a static Type System. We can call it statically typed GraphQL. In addition, the community has a GraphQL practice for dynamic typing.

graphql-anywhere: Run a GraphQL query anywhere, without a GraphQL server or a schema.  https://github.com/apollographql/apollo-client/tree/master/packages/graphql-anywhere

It differs from statically typed GraphQL in that it does not have a schema-based data shape validation phase. Instead, it mindlessly fires the Resolver function based on the fields in the Query statement.

It also doesn’t care if the data type returned by the Resolver function is correct. A field does not have to be defined before it can be consumed by the front end; it can be calculated dynamically.

Dynamically typed GraphQL has some convenience in some scenarios. However, it also loses part of the essence of GraphQL, which will be described in more detail later.

It is worth mentioning that both statically typed GraphQL and dynamically typed GraphQL can be run on both the server side and the front end.

4.5 GraphQL does not necessarily return JSON data format

That’s another interesting fact. We first demonstrated how to reverse the GraphQL query syntax design based on JSON data results. Now we say that GraphQL can return no JSON data format.

That’s right. When a new thing appears, with its continuous development, it can be divorced from its original intention, derived from different forms.

Again, this is an example from GraphQL-Anywhere.

Here, it implements a gqlToReact Resolver that converts a GraphQL query to a ReactElement structure.

Not only dynamically typed GraphQL has this capability, but statically typed GraphQL also has the potential to achieve the same effect.

But this approach, so far only at the demonstration stage. Its clever use still waits for the community to excavate and explore.

How to use GraphQL

So far, we’ve seen GraphQL’s high degree of freedom and flexibility. When constructing GraphQL Server, different modes can be adopted according to actual requirements and scenarios.

5.1 a RESTful – Like model

The pattern is simply to replace the RESTful API service with the GraphQL implementation. There are as many GraphQL services after refactoring as there were RESTful services before. It is a simple one-to-one relationship.

By default, queries to both GraphQL services are issued with two requests instead of one. Here’s an example:

When the front end needs product data, it changes from calling product-related RESTful API to querying product-related GraphQL. However, when you need order-related data, you might want to query another GraphQL service.

Some companies have done this with GraphQL; Use GraphQL for specific services.

However, this pattern does not take advantage of GraphQL’s ability to merge and correlate requests. Only play on demand query, accurate query field role, value is limited.

Therefore, after they practice, they find little effect; GraphQL is nothing more than a simple and mature API architecture.

In fact, this is a kind of selection error.

5.2 GraphQL as an API Gateway mode

In this mode, GraphQL takes over a whole block of data query requirements from the front end.

The front-end no longer directly calls specific RESTful interfaces, but indirectly obtains product, order, search and other data through GraphQL.

In the middle layer of GraphQL, we integrate various micro-services into a data relation network based on GraphQL Schema according to their data association. The front end can initiate data acquisition, screening and clipping of multiple microservices through GraphQL query statements at the same time.

It’s worth noting that the GraphQL service, as the API Gateway, can initiate query requests to the previously mentioned RESTful GraphQL from within its Resolver.

In this way, not only the one-to-many problem is avoided in the front end, but also the internal redundancy problem that API Gateway GraphQL needs to request RESTful full data interface is solved. Data calls from service to service can also be made more precise.

The GraphQL service is a data consumer friendly schema. The data consumer can be either the front end or other services.

When data consumers are other services, the GraphQL query statements can be used to obtain data more accurately from each other, avoiding redundant data transfer and interface calls.

When the data consumer is the front end, because the front end has to deal with multiple data providers, each data provider is a separate GraphQL, which is not substantially improved. Having a GraphQL for the Gateway role at this point can really reduce the complexity of front-end calls.

5.2.1 Two types of GraphQL API Gateway services

The GraphQL service, which also plays the role of API Gateway, is also classified differently in terms of implementation.

1) GraphQL with lots of real data manipulation and processing

2) Forward data requests and aggregate GraphQL of data results

First, back-end services in the traditional sense; The second type, which is our focus today, is GraphQL as BFF.

The requirements of these two types of GraphQL services are different, the former may contain a lot of CPU intensive computation, while the latter is mostly Network I/O related behavior in general.

Many companies don’t advocate using Node.js to build the first service, whether it’s RESTful or GraphQL. So do we.

Therefore, the GraphQL we discuss later, if not explicitly stated, can be understood as the second type mentioned above.

5.3 GraphQL as a Backend Framework

In clearing up the myths about GraphQL, we pointed out that GraphQL can not be a Server.

This means that a Server that includes a GraphQL implementation does not necessarily interact with the front and back data through GraphQL queries, and it can continue to use RESTful APIS.

In other words, we can use GraphQL as a server-side development framework and then issue GraphQL queries in various RESTful interfaces.

Neither the front-end nor any other back-end service needs to know that GraphQL exists. The front end is called by a RESTful API, and inside the RESTful service, it issues GraphQL queries to itself.

So, what are the benefits and values of this model?

Imagine that you implement BFF in a RESTful API style. Due to different scenarios on PC and mobile terminals, the consumption methods of the same data differ greatly.

On the PC side, it can request the full amount of data at once.

On mobile, because of its small screen, it has to request data multiple times. First screen once, non-first screen once, scroll and load N times as required, M times in multiple 2-level pages.

We can either implement a superinterface that ADAPTS to different scenarios based on the request parameters (i.e., implement a half-baked GraphQL); Or implement multiple RESTful interfaces with similar but different functions.

The differences are so great that many companies simply divide BFF into TWO BFF services, PC-BFF and mobile-BFF.

We can combine PC-BFF and mobile-BFF into a graphQL-bFF service. Even if the front and back ends don’t interact with GraphQL query statements, we can write relatively simple query statements in each interface instead of the more expensive interface implementation.

That is to say, when GraphQL is used to build BFF, there will be conflicts in the division of labor and communication between the front and back ends. We can downgrade the GraphQL service to a RESTful service by simply writing queries that need to be written on the front end to the back end interface.

If you implement a RESTful service, converting to GraphQL is not that easy.

With this ability to gracefully degrade, we feel more comfortable pushing forward with the GraphQL-BFF scheme.

The essence of GraphQL

Understanding the essence of GraphQL can help you put GraphQL into practice.

Why GraphQL is called GraphQL, and where is the Graph represented?

The query statement of GraphQL looks like a simplification of JSON writing. JSON is a Tree data structure. Why not call it TreeQL instead of GraphQL?

6.1 the Graph of the Tree VS

One important pre-knowledge is, what is a Tree, what is a Graph, and how are they related?

The diagram below shows the structure of a Tree.

Tree has one and only one Root node and one and only one parent node for each non-root node. They form a hierarchy. Any two nodes have only one connection path; There are no loops and no recursive references.

The Graph below shows the structure of a Graph.

There could be more than one connection path between nodes in a Graph, there could be loops, there could be recursive references, there could be no Root node. They form a network structure.

We can take Graph, a network structure, and compress it into a simplified form with only one connection path for any node by clipping the connection path. So the network structure degenerates into a hierarchy, which becomes a Tree.

In other words, Graph is a more complex data structure than Tree, which is a simplified form of it. With Graph, we can generate different trees in different clipping ways. The information contained in the Tree is not enough to build a sufficiently complex Graph structure without adding additional data.

6.2 Graph structure in GraphQL

In GraphQL, it is not GraphQL query statements that undertake the construction of network structure, but the Schema built based on GraphQL Type System.

The GraphQL Schema defines five data types A, B, C, D, and E, which are mounted to the A, B, C, D, and E fields of the entry type Query.

A, B, C, D, and E contain recursive structures. A contains B and C, B contains C and D, D contains E, E contains A, and back to A.

It’s a complex web of relationships. You don’t have to be this complicated to build recursive associations. B is directly in A, and A is in B, so here’s A demonstration.

With this Graph relational network based on data types, we can implement the ability to derive JSON trees from graphs.

The graph above shows a GraphQL query, which is a hierarchy of keys, i.e. a Tree.

It takes field A from the root node, and then layers down to find e. The e node also contains a field a of the same type as the root node, so it can continue to layer down and start all over again until it reaches the E node, where it only takes the data field and the query is aborted.

I wrote a simple Resolver function to demonstrate the results of the query.

It’s simple. Query returns the same letters as the field name, and any child data is a string of letters concatenated with the parent node. This way we can see the hierarchy of data flow from the query results.

The query results are as follows:

In the data field of the first E node, the data of the parent node is obtained, and the data of the parent node is obtained from its parent node, so there is a data chain.

And the second e node, similarly, has two chains.

We can stay in the data field of any node as long as we don’t write subsequent fields.

In other words, we used the Query statement as a Tree to crop the Schema data association network as a Graph to get the JSON structure we wanted.

From this perspective, we can understand why GraphQL does not allow Query statements to remain of type Object. It must explicitly write out the fields inside the Object until all Leaf nodes are Scalar.

This isn’t just a so-called best practice, it’s a feature of Graph itself. Object nodes can be extended to infinitely large data structures through loops or recursive relationships. The Query statement must be written cleanly to help GraphQL crop out unnecessary data association paths.

6.3 Actual value of Graph network structure

The previous cases A, B, C, D, and E do not give you an intuitive sense of the actual value of Graph network structure. It looks like a matchmaking game.

Its necessity and value are highlighted in the context of Facebook’s social networking.

Suppose we want to get the user’s friends’ friends’ friends’ friends’ friends’ friends’ friends’ friends at once, do we have a particularly good method based on RESTful apis? It’s hard to say.

Graph, with its recursive, relational structure, makes this query a breeze.

We define a User type that hangs in the User field on the Query entry. The Friends field of type User is again a list of types User. This builds a recursive association.

The getFriends query statement, which can continuously start with any user and associate its friends to get an array of friends results. Any friend is also a User, and it has its own friends. The query stops at the outermost friends and only queries the ID and name fields.

Here’s another classic GraphQL misconception: Only social networks like Facebook and Twitter work for GraphQL, and in our case, GraphQL doesn’t.

Just because GraphQL is particularly effective in social networking doesn’t mean GraphQL isn’t profitable in other contexts.

Imagine an e-commerce platform with an iron triangle of users, products and orders, not to mention other inventory, prices, coupons, collections, etc. GraphQL still works in the simplest scenarios.

We have constructed three types of User, Product and Order, which have recursive correlation relation in field among each other, as a Graph structure. On the Query entry type, there are user, Product, and Order fields respectively.

Based on this, we can realize rich and flexible queries from any dimension of user, Product and order through their association relations.

For example, to view all orders of the user and the products associated with the order, the following statement would be used:

We query the user whose ID is 123, his name and order list. For each order, we obtain the creation time, purchase price and associated products of the order. For the products associated with the order, we obtain the product ID, product title, product description and product price.

When our back-end staff organization is structured according to the domain model, users, products, and orders are typically three teams, each providing domain-specific interfaces. GraphQL makes it easy to put them together.

For example, to Query all orders placed by a product and their associated users, the following statement would be used:

We have inquired the product id 123, its product title, product description and price, as well as the associated order. For each associated order, we queried when the order was created, the purchase price, and the user who placed the order, and for the user who placed the order, we queried his user ID and name.

As you can see, once you build a graph-structured data network, it doesn’t have a single Root node like a Tree. From any entry point, it can continuously derive data by associating paths to get JSON results.

We don’t have to write interfaces for product detail pages, order-detail pages, user information. We have written a network of data relationships that can be adapted to different scenarios.

What is demonstrated here is only the relational network of user, product and order resources. You can already see the applicability of GraphQL. In a real world scenario, we can build more complex data networks with more powerful data expression capabilities that can bring more benefits to our business.

Our GraphQL-BFF practice mode

With that in mind, let’s take a look at graphQL-bFF in action.

First is the selection of technology, we mainly use the following technology stack.

The development language is TypeScript, running on Node.js V10.x, and the server framework is Koa v2.x. Apollo-server-koa module is used to run GraphQL service.

Apollo-graphql is one of the most well-known and mature GraphQL frameworks in the Node.js community. A lot of details were done, and there were some relatively cutting-edge explorations, such as Apollo Federation architecture.

However, two points are worth mentioning:

Apollo-graphql is part of the GraphQL community, not the official Facebook GraphQL development team. Apollo-graphql builds on the official GraphQL concept with its own packaging and design features. Even the official GraphQL developers have reservations about something as radical as Apollo Federation so far.

2) Apollo-GraphQL focuses on the GraphQL services of the API Gateway role of the first category mentioned above, and this article discusses the second category. So there were a lot of features in Apollo-GraphQL that weren’t necessary for us, and some of them were used in ways that didn’t fit our scenario.

We mainly used the graphQL-Tools and Apollo-Server-KOA modules of Apollo-GraphQL, based on which, we designed and adapted them in accordance with our scene.

7.1 Our GraphQL-BFF architecture design

The core idea of GraphQL-BFF is to integrate multiple services into a centralized data graph.

The data structure contract for each service is put into a large and complete GraphQL Schema. Without any modularization and decoupling, the development experience would be terrible. Every team member, to modify the same Schema file.

This is clearly unreasonable. The graphQL-BFF development pattern should have a one-to-one correspondence with the service domain model. Then, in some form, the services are naturally integrated together.

Therefore, we designed the concept of GraphQL-Service.

7.1.1 GraphQL – Service

Graphql-service is a JS module composed of two parts: Schema + Resolver, which corresponds to a Servcie at the back end of the domain model. Each GraphQL-Service should be written in a modular manner, and combined with other GraphQL-services to build a larger GraphQL-Server.

Graphql-service builds data association relationships with GraphQL’s Type Extensions.

As shown above, our UserService only deals with user-related type handling. It defines its own base fields, ID and name. Extend Type defines its associated fields in the Order and Product data, as well as entry fields defined in the Query.

As you can see from User Schema, User has two types of query paths.

1) Obtain User information by passing parameters through the root node Query.

2) User information can be obtained by data association through Product or Order node.

The diagram above shows the Schema for OrderService, which also only deals with the order-related type logic. Extend Type also defines the associated fields in User and Product, as well as the entry fields in the root node Query.

Order data, like User, can be consumed in two ways. One is through Query nodes and the other is through data association nodes.

When we demonstrated the triangulation of User, Order, and Product, we wrote their associations in the same Schema. When we combine multiple GraphQL-service schemas together, we can generate the same result:

The graph above was not written by us manually, but is the result of merging multiple GraphQL-Service schemas. As you can see, it’s basically the same as the handwritten version.

It is not enough to have a decoupled Schema, which only defines data types and their associations. We need a Resolver to define how to get the data, and a Resolver also needs to be decoupled.

7.1.2 GraphQL – Resolver

In both the official GraphQL documentation and apollo-GraphQL documentation, Resolver appears as a normal function.

This is fine in simple scenarios. As in a simple scenario, a server can be created using node.js’s HTTP.createserver.

As shown above, set the status code, set the content-Type of the response, and return the Content.

However, in a more complex real project, we actually need a server-side framework like Express, KOA, etc., to write our server-side processing logic in middleware patterns, which the framework integrates into a requestListener function, Register with http.createServer(requestListener).

The fact that/GraphQL is the only endpoint in GraphQL Server does not mean that it requires only a set of Koa middleware.

As we pointed out at the beginning, each superinterface contains a GraphQL implementation of half the functionality. GraphQL is one step closer to being a super interface than simply thinking of it as a normal interface.

Each field under Query may correspond to one or more internal service apis for invocation and processing. A resolverMap composed only of ordinary functions is not sufficient to fully express its logical complexity.

Whether resources are represented by an endpoint or GraphQL Field, they are only slightly different externally and do not change the complexity of the business logic.

Therefore, middleware with better expression ability than ordinary functions is used to combine one Resolver after another and then integrate it into a ResolverMap. Can better solve problems that were previously impossible or difficult to solve.

The ability to build is about understanding the complexity and nature of the problems we face, and being able to select and design appropriate models of program expression.

Later we will show how the right architecture can easily overcome previously unsolvable problems.

7.1.3 Koa-compose organizes our Resolver

It may not be clear to many students that the middleware patterns in Express or KOA can be used independently of them as server-side frameworks. Just as GraphQL can stand alone as a server and be used anywhere that supports JavaScript running.

We will use the KOa-compose NPM module to construct our Resolver.

The GQL function takes a Schema and returns a graphqL-service. Each graphQL-service has a resolve method:

The resolve method, which takes two arguments. The first is typeName, which corresponds to the Type name of the Object Type in graphQL-schema. The second is fieldHandlers, each of which supports the middleware pattern and will eventually be integrated into a Resolver by Koa-Compose.

In the case of UserService, the Resolver reads as follows:

All parameters received by the Resolver as a normal function are incorporated into CTX. Ctx.result is the final output of this field, similar to ctx.body in KOA Server. We deliberately use the ctx.result property, which is different from ctx.body, to make it clear whether we are dealing with an interface or a field.

In simple scenarios, the Resolver of middleware mode is different from the Resolver of ordinary function only in the number of parameters and the way of return value. It doesn’t add a lot of code complexity.

When we want to reuse the same logic for multiple fields, we write it into middleware, and then turn the handler into an array. (In the code we use JSON to simulate the database table, so it is synchronous code, in the real project, it can be asynchronous call interface or query database).

Logger, above, is just a simple example. In addition, we can write requireLogin middleware to determine if a field is only available to logged in users. We can write different tool-based middleware to inject ctx.fetch, CTx. post, ctx. XXX and other methods for subsequent middleware to use.

Each GraphQL Field has a separate set of middleware and CTX objects that do not interact with the other fields. We can also put the middleware that all fields share into the middleware in the KOA Server.

As shown in the figure above, the green box is the endpoint, and you can write middleware at the KOA Server level. The blue box is a GraphQL Field that lets you write middleware at the Resolver level. Changes to CTX from middleware at the endpoint level affect all subsequent fields.

In other words, we could do something like this. Logger at the interface level can know the time of the entire GraphQL query. Write a piece of middleware that, before next, mounts some methods for subsequent middleware to use; After Next, get the graphQL query results for additional processing.

7.2 Solving Mock Puzzles

GraphQL is inherently mock friendly because its Schema already specifies all data types and their associations; It’s easy to automatically generate fake data by type through something like Faker Data.

However, there are many challenges to implement GraphQL Mocking in practice.

As you can see above, mocks in Apollo GraphQL look simple. You just need to set the mock to true when creating the service, or provide a mock Resolver. However, a global mock that follows service creation is too crude.

The value of mock is in providing better data flexibility to speed up development efficiency. It can provide fake data when there is no data; It can also be degraded to fake data without restarting the service when there is a problem with the interface of real data. It can be a whole GraphQL query level mock or a field level mock.

As a superinterface to the GraphQL service, there is little point in global, mocking setup at startup.

Apollo GraphQL mocking practice is the same thing that it uses ordinary functions to describe Resolver. It is difficult to simply extend a resolver to support mocking. It has to add a mock Resolver map to perform mocking functions when creating a service.

Our composed Resolver is simple to handle dynamic mocking.

Not only can it be determined dynamically at run time, it can be refined down to the field level, it can even follow the mock logic of a query (by adding the @mock directive).

This is mock data generated by default based on the faker NPM package based on the data type.

There is a simple internal implementation of mocking by default in our design. We first wrote the figure above and called the corresponding method of faker module according to GraphQL Type to generate fake data.

Then, in createResolver, the function that integrates middleware into resolver, determine whether there is any custom mock Handler function in the middleware. If not, append the mocker handler written earlier.

 

We also provide mock middleware that allows developers to specify mock data sources, such as a mock JSON file.

The Mock middleware, when receiving string arguments, searches for a file with the same name in the local mock directory as the return value of the current field. It also takes functions as arguments in which we can manually write more complex mock data logic.

The interesting thing is that the mock/user.json file contains only the data in the red box above, and its associated Collections field is real. This is the logical thing to do and the mock should follow the resolver. Associated fields have their own resolvers and may invoke their own interfaces. Children should not enter mock mode just because the parent node is mock.

In this way, we can mock out the parent resolver’s backend interface after it fails to make the child resolver run properly. If we want the child node resolver to enter the mock as well. It’s as simple as adding an @mock directive.

As shown above, the Resolvers for both the User field and the Collections field enter the mock pattern.

The mock Resolver function is customized as shown above, and the Mock middleware ensures that the Mock Resolver function is executed only when the field enters the mock mode. And there is still a chance that the mock resolver function will trigger the real data retrieval logic behind it by calling the next function.

All of the above flexibility comes from the fact that we choose the middleware mode with better expressiveness and composability to replace the ordinary function and assume the function of resolver.

conclusion

At this point, we have a simple and flexible practice pattern. We use Schema to build Data Graph, and Middleware to build Resolver Map, both of which have strong expressive power.

In the development of GraphQL-bFF, our GraphQL-Service has an overall one-to-one correspondence with the back-end Service based on the domain model. There is no awkwardness of re-coupling in the GraphQL layer after the back-end data layer is decoupled.

There are many more topics to discuss about GraphQL, such as batching, caching and so on. This section is available in many GraphQL documentation and tutorials on the web, so we won’t go over it here.

In general, based on our observations and practices with GraphQL, we believe it can solve the problems we face better than RESTful apis.

Our expectations for GraphQL go beyond the BFF layer. We hope that the successful experience of using GraphQL on the BFF layer will help us find a reasonable design for using GraphQL pattern on the Micro Frontend architecture.

As demonstrated earlier, common level data types such as User, Product, and Order cannot be maintained by just one team; they need to be extended by other teams. It allows us to find the orders, favorites, coupons and other data maintained by other teams through users.

In the Micro Frontend architecture, a payment button contains multiple types of data, including product information, user information, inventory information, UI display information, and interactive status information. When the payment button is clicked, sufficient data can be obtained to decide whether to pay or not.

The simple design of Micro Frontend uses Vue, React, and Angular frameworks to maintain different components and communicate with each other through router/message-passing. In my opinion, this is a parody of the back-end microservices architecture.

Back-end services, each deployed in a separate environment, are volume insensitive; Therefore, different languages and technology stacks can be used. That doesn’t mean it’s just a matter of putting it in the front end. Not being able to share the infrastructure of front-end development is not a microfront-end, it’s an organizational mess of people.

GraphQL shows us that a microfront-end architecture based on the domain model might be a better direction. A simple payment button, also integrated with multiple domain models, was developed by multiple developers in an organized way. Just because it appears to be a Button component, it is not maintained by a single team.

Of course, exploring GraphQL in other directions is only possible if the GraphQL-BFF architecture is successfully verified. With regard to the current stage of practical results, we are full of confidence in this.

Although we have no open source plans for our code, we believe that this article is a complete and clear introduction to our GraphQL-BFF solution. I hope it helps a little bit.

【 Recommended reading 】

  • Cross the multi-business line challenge, Ctrip order index service 1.0 to 2.0

  • React Native Clean architecture practice

  • React Hook implementation principles and best practices

  • Daily deployment 6000 times, Ctrip continued to deliver and build platform practice

  • How to achieve front-end business decoupling, Ctrip hotel query home page 1.0 to 3.0