This article analyzes GraphQL's unique capabilities in API design, highlighting its advantages over REST, such as optimized data loading, enhanced data model composability, and improved semantic clarity in queries and mutations.
GraphQL is a query language for APIs proposed by Facebook. Many consider it as an alternative to REST, while others find GraphQL more complex than REST with no clear benefits. What unique capabilities does GraphQL have beyond the scope of REST? Is there an objective and rigorous criterion to help us make a judgment?
Nop Platform, through rigorous mathematical reasoning, has reinterpreted the positioning of GraphQL, resulting in new design ideas and technical implementation solutions. Under this interpretation, the NopGraphQL engine achieves comprehensive superiority over REST, strictly speaking, in the mathematical sense.
In simple terms, GraphQL can be seen as an improvement of REST in a pull mode, to some extent, it reverses the flow of information.
Through mathematical equivalence transformations (form transformations), we can clearly see that GraphQL is essentially supplementing a standardized
@selection parameter to REST, allowing selective fetching of results.
Traditional REST is equivalent to pushing all information from the backend to the frontend, where the frontend cannot finely participate in the information production and transmission process. In the implementation of NopGraphQL, if the frontend does not request the
@selection parameter, it automatically degrades to the traditional REST model, returning all non-lazy fields. Lazy fields need to be explicitly specified for them to be returned to the frontend.
In the architecture of Nop Platform, GraphQL is not just a transport protocol on par with REST; it is a universal mechanism for information decomposition and composition. It helps us organize the information structure of the system more effectively.
The positioning of NopGraphQL is as a general decomposition and dispatch mechanism for backend service functions. The same service function can be simultaneously published as a REST service, GraphQL service, gRPC service, Kafka message service, Batch processing service, etc. In simple terms, any scenario that involves receiving a Request Message and returning a Response Message can be directly integrated with the NopGraphQL engine as its implementation mechanism.
Using GraphQL has the following advantages compared to traditional REST:
If the frontend does not require the total to be returned, the calculation of the total property can be skipped.
In traditional REST services, service functions directly return DTO objects, and all properties of DTO objects must be loaded in the service function. For example, if a virtual roles field is added to the NopAuthUser entity, it needs to be added to the DTO, and all places that return NopAuthUserDTO need to supplement the loading logic for the roles field. In NopGraphQL engine, the object returned by the service function is not directly serialized into JSON and returned to the frontend. Instead, it is processed further by the GraphQL engine using DataLoader to obtain the final returned data. This can significantly improve the composability of backend data models.
In the example above, the findList and get functions only need to know how to load NopAuthUser objects, without needing to know how NopAuthUser is associated with NopAuthRole objects. Service functions can directly return entity objects without the need for manual translation into DTO objects. When we add a dynamic property roles to the NopAuthUser type through the
@BizLoader mechanism, all places in the returned result that involve NopAuthUser automatically gain the roles property. The knowledge provided by DataLoader, through the action of the NopGraphQL engine, automatically combines with the functions we manually write, such as get/findList.
In NopGraphQL, we can also use an additional xmeta metamodel file to independently control the permissions, transformation, and validation logic of each field, simplifying and standardizing the implementation of service functions.
In Domain Driven Design (DDD), a critical and important design for information space planning is the concept of aggregate roots. Aggregate roots act as core nodes in the information space, allowing traversal in the information space by establishing direct connections with a few core nodes. Aggregates organize information in a form on the surface, reducing cognitive and usage costs, but it also has a negative impact on performance. GraphQL's selection capability is a complementary ability to the ability to aggregate (aggregation and selection as dual operations should be paired in design). It allows us to selectively extract slices of information we need from a vastly complex information structure, precisely meeting our business requirements.
Although REST specifies GET and POST methods, due to limitations at the implementation level (GET method does not support passing data through the HTTP body, URL length limitations, and security issues), we often cannot accurately differentiate the semantics of service functions through GET/POST. In the NopGraphQL framework, it is conventionally agreed that queries have no side effects and do not modify the database, while mutations have side effects and require consideration of transaction management, etc. Therefore, in the NopGraphQL framework, there is no need to manually add the
@Transactional annotation for each service function; instead, a unified transaction environment is established for all mutation operations. Through optimizations at the implementation level, if a mutation does not actually access the database, the NopGraphQL framework does not actually obtain a database connection.
The NopGraphQL engine decomposes the execution of backend service functions into two phases: 1. Execute the service function, automatically establishing a transaction environment for mutations. 2. Process the result object returned by the service function, performing data transformation and pruning through DataLoader.
After the first phase is completed, the transaction is automatically closed, and some resource-consuming data loading tasks do not start executing. In the second phase, the transaction is closed, and the OrmSession is set to readonly. If entity data is inadvertently modified, an exception is automatically thrown. In this way, we can reduce the time the transaction is open and the time the database connection is occupied.