Everyone is an API Designer: My thoughts on RESTful APIs, GraphQL, RPC APIs

Original address: Liang Guizhao's blog

Blog address: http://blog.720ui.com

Welcome to the official account: "Server Thinking". A group of people with the same frequency, grow together, improve together, and break the limitations of cognition.

I haven't written many articles for a while, so today I will write my own thoughts on API design. First of all, why write about this topic? First, I have benefited a lot from reading the article "Ali Researcher Gu Pu: Thoughts on Best Practices in API Design". The article reprinted two days ago also aroused the interest of readers. I think I should put my own The thoughts are compiled into a document to share and collide with everyone. Second, I think I can solve this topic within half an hour, and try to turn off the lights before 1 o'clock to sleep, haha.

Now, let's talk about API design. I will throw out a few points, welcome to explore.

1. Well-defined specifications have succeeded more than half of the time

Normally, the norm is the standard agreed upon by everyone. If everyone abides by this standard, the natural communication cost will be greatly reduced. For example, everyone hopes to learn from Ali's specifications and define several domain models in their own business: VO, BO, DO, and DTO. Among them, DO (Data Object) corresponds to the database table structure one by one, and the data source object is transmitted upward through the DAO layer. The DTO (Data Transfer Object) is a remote call object, which is the domain model provided by the RPC service. For BO (Business Object), it is an object that encapsulates business logic in the business logic layer. In general, it is a composite object that aggregates multiple data sources. Then, VO (View Object) is usually the object transmitted by the request processing layer. After it is converted by the Spring framework, it is often a JSON object.

image.png

In fact, if the domain models of DO, BO, DTO, and VO are not clearly defined in such a complex business as Alibaba, its internal code will easily become confused. The internal RPC adds a manager layer on top of the service layer. To achieve internal norm unification. However, if it's just a separate domain and doesn't have many external dependencies, then don't design so complex at all unless it is expected that it might become large and complex. In this regard, it is particularly important to adapt to local conditions in the design process.

Another canonical example is a RESTful API. In the REST architectural style, each URI represents a resource. Therefore, a URI is a unique resource locator for the address of each resource. The so-called resource is actually an information entity, which can be a piece of text, a file, a picture, a song, or a service on the server. RESTful API specifies operations on server-side resources through GET, POST, PUT, PATCH, DELETE and other methods.

【GET】          /users                 # 查询用户信息列表
【GET】          /users/1001            # 查看某个用户信息
【POST】         /users                 # 新建用户信息
【PUT】          /users/1001            # 更新用户信息(全部字段)
【PATCH】        /users/1001            # 更新用户信息(部分字段)
【DELETE】       /users/1001            # 删除用户信息

In fact, there are four levels of RESTful API implementation. The first level (Level 0) of the Web API service just uses HTTP as the transport. The second level (Level 1) of Web API services introduces the concept of resources. Each resource has a corresponding identifier and expression. The third level (Level 2) Web API services use different HTTP methods to perform different operations, and use HTTP status codes to indicate different results. The fourth level (Level 3) Web API service uses HATEOAS. Link information is included in the representation of the resource. Clients can discover actions that can be performed based on the link. Typically, pseudo-RESTful APIs are designed based on the first and second layers. For example, we use various verbs in our Web API, such as get_menu and save_menu , and a real RESTful API needs to satisfy the third level and above. If we follow this set of specifications, we are likely to design an API that is easy to understand.

Note that with a well-defined specification, we've been more than halfway there. If this set of specifications is the industry standard, then we can practice boldly, don't worry that others will not use it, just leave the industry standard to him and study it. For example, Spring has played a pivotal role in the Java ecosystem, and if a newcomer does not understand Spring, it is a bit overwhelming. However, in many cases, due to business constraints and the company's technology, we may use pseudo-RESTful APIs based on the first-level and second-level designs, but it is not necessarily backward or bad. the cost of learning. Many times, we try to change the team's habits to learn a new specification, and the benefits (input-output ratio) are very small, and the gains outweigh the losses.

To sum up, the purpose of a well-defined specification is to reduce the cost of learning and make the API as easy to understand as possible. Of course, there are other ways to design an API that is easy to understand. For example, the name of the API we define is easy to understand, and the implementation of the API is as general as possible.

2. Explore the compatibility of API interfaces

API interfaces are constantly evolving. So we need to adapt to change to some extent. In a RESTful API, the API interface should be as compatible with the previous version as possible. However, in the actual business development scenario, the existing API interface may not support the adaptation of the old version with the continuous iteration of business requirements. At this time, if the API interface of the server is forcibly upgraded, the old function of the client will fail. In fact, the web side is deployed on the server, so it can be easily upgraded to adapt to the new API interface of the server side. However, other clients such as Android side, IOS side, PC side, etc. run on the user's machine. Therefore, it is difficult for the current product to adapt to the API interface of the new server, resulting in functional failure. In this case, the user must upgrade the product to the latest version to use it normally. To address this version incompatibility, a practical approach in designing RESTful APIs is to use version numbers. In general, we will keep the version number in the url and be compatible with multiple versions at the same time.

【GET】  /v1/users/{user_id}  // 版本 v1 的查询用户列表的 API 接口
【GET】  /v2/users/{user_id}  // 版本 v2 的查询用户列表的 API 接口

Now, without changing the API interface for querying the user list of version v1, we can add the API interface for querying the user list of version v2 to meet new business needs. At this time, the new function of the client's product will request new The API interface address of the server. Although the server will be compatible with multiple versions at the same time, maintaining too many versions at the same time is a big burden for the server, because the server needs to maintain multiple sets of codes. In this case, a common practice is not to maintain all compatible versions, but to maintain only the latest several compatible versions, for example, to maintain the latest three compatible versions. After a period of time, when the vast majority of users upgrade to a newer version, the old API interface versions of some servers that are used less frequently are discarded, and users who use very old versions of the product are required to be forced to upgrade. Note that "the API interface for querying the user list in version v1 does not change" mainly refers to the fact that it appears to be unchanged to the caller of the client. In fact, if the business changes too much, the developer of the server needs to use the adapter mode for the old version of the API interface to adapt the request to the new API interface.

Interestingly, GraphQL offers a different idea. In order to solve the problem of service API interface explosion and aggregate multiple HTTP requests into one request, GraphQL proposes to expose only a single service API interface, and multiple queries can be performed in a single request. GraphQL defines an API interface, which we can call more flexibly on the front end. For example, we can select and load fields that need to be rendered according to different businesses. Therefore, the full number of fields provided by the server can be obtained by the front end on demand. GraphQL can add new functionality by adding new types and new fields based on those types without causing compatibility issues.

image.png

In addition, in the process of using the RPC API, we need to pay special attention to compatibility issues. The second-party library cannot rely on parent. In addition, SNAPSHOT can be used for local development, but it is forbidden to use it in the online environment to avoid changes and lead to version incompatibility problems. We need to define a version number for each interface to ensure that the version can be upgraded in case of subsequent incompatibility. For example, Dubbo suggests that the third-digit version number usually indicates a compatible upgrade, and only changes to the service version are required if they are not compatible.

For the canonical case, we can look at k8s and github, where k8s uses RESTful API, and github part uses GraphQL.

3. Provide a clear mental model

The so-called mental model, my understanding is that it is aimed at the abstract model of the problem domain, has a unified understanding of the function of the domain model, constructs a realistic mapping of a certain problem, and divides the boundaries of the model, and one of the values ​​of the domain model is to unify the thinking, Clear boundaries. Assuming that everyone does not have a clear mental model, and there is no unified understanding of API, then the real problems in the picture below are likely to occur.image.png

4. Shield business implementation in an abstract way

I think a good API interface is abstract, so it needs to shield the business implementation as much as possible. So, the question is, how do we understand abstraction? In this regard, we can think about the design of java.sql.Driver. Here, java.sql.Driver is a specification interface, and com.mysql.jdbc.Driver is the implementation interface of mysql-connector-java-xxx.jar to this specification. Then, the cost of switching to Oracle is very low.

Under normal circumstances, we will provide external services through API. Here, the logic of the interface provided by the API is fixed, in other words, it is generic. However, when we encounter a scenario with similar business logic, that is, the core backbone logic is the same, but the implementation of the details is slightly different, what should we do? Many times, we choose to provide multiple API interfaces for different business parties to use. In fact, we can do it more elegantly with the SPI extension point. What is SPI? The full English name of SPI is Serivce Provider Interface, which is a service provider interface. It is a dynamic discovery mechanism that can dynamically discover the implementation class of an extension point during program execution. Therefore, specific implementation methods of the SPI are dynamically loaded and called when the API is called.

At this point, have you thought of the template method pattern? The core idea of ​​the template method pattern is to define the skeleton and transfer the implementation. In other words, it defers the concrete implementation of some steps to subclasses by defining the framework of a process. In fact, in the process of landing microservices, this kind of thinking also provides us with a very good theoretical basis.

image.png

Now, let's look at a case: the unshipped goods in the e-commerce business scenario are only refunded. This situation is very common in e-commerce business. After users place an order and pay for it, they may apply for a refund for various reasons. At this time, because no return is involved, the user only needs to apply for a refund and fill in the reason for the refund, and then let the seller review the refund. Then, since the refund reasons for different platforms may be different, we can consider implementing it through the SPI extension point.

SPI Extension Case - Refund only if not shipped.png

In addition, we often use the factory method + strategy pattern to shield external complexity. For example, if we expose an API interface getTask(int operation), we can create instances through factory methods and define different implementations through strategy methods.

@Component
public class TaskManager {

    private static final Logger logger = LoggerFactory.getLogger(TaskManager.class);
    
    private static TaskManager instance;

    public MapInteger, ITask> taskMap = new HashMap<Integer, ITask>();

    public static TaskManager getInstance() {
        return instance;
    }

    public ITask getTask(int operation) {
        return taskMap.get(operation);
    }

    /**
     * 初始化处理过程
     */
    @PostConstruct
    private void init() {
        logger.info("init task manager");
        instance = new TaskManager();
        // 单聊消息任务
        instance.taskMap.put(EventEnum.CHAT_REQ.getValue(), new ChatTask());
        // 群聊消息任务
        instance.taskMap.put(EventEnum.GROUP_CHAT_REQ.getValue(), new GroupChatTask());
        // 心跳任务
        instance.taskMap.put(EventEnum.HEART_BEAT_REQ.getValue(), new HeatBeatTask());
        
    }
}

Another design to shield internal complexity is the appearance interface, which encapsulates and integrates the interfaces of multiple services and provides a simple calling interface for clients to use. The advantage of this design is that the client no longer needs to know the interface of so many services, and only needs to call this facade interface. However, the disadvantages are also obvious, that is, the business complexity of the server is increased, the interface performance is not high, and the reusability is not high. Therefore, according to local conditions, ensure a single responsibility as much as possible, and perform "Lego-style" assembly on the client side. If there are SEO-optimized products that need to be included by search engines like Baidu, you can generate HTML through server-side rendering when the first screen is displayed, so that it can be included by the search engine. If it is not the first screen, you can use the client Call the server-side RESTful API interface for page rendering.

Also, with the popularity of microservices, we have more and more services, and many smaller services have more cross-service calls. Therefore, the microservices architecture makes this problem more common. To solve this problem, we can consider introducing an "aggregation service", which is a composition service that combines data from multiple microservices. The advantage of this design is that some information is integrated through an "aggregation service" and then returned to the caller. Note that an "aggregation service" can also have its own cache and database. In fact, the idea of ​​aggregation services is everywhere, such as serverless architectures. We can use AWS Lambda as the computing engine behind the serverless service in practice, and AWS Lambda is a function-as-a-Servcie (FaaS) computing service, we directly write the software running on the cloud function. Then, this function can assemble existing capabilities to do service aggregation.

image.png

Of course, there are still many good designs, and I will continue to supplement and discuss them in the public account.

Five, consider the performance behind

We need to consider the performance problems of the database caused by various combinations of input fields. Sometimes, we may expose too many fields for external combination use, resulting in a full table scan without the corresponding index in the database. In fact, this situation is especially common in query scenarios. Therefore, we can only provide the indexed field combination to the external call, or in the following case, require the caller to fill in the required taskId and caseId to ensure that our database uses the index reasonably and further ensure the service performance of the service provider.

ResultVoid> agree(Long taskId, Long caseId, Configger configger);

At the same time, asynchronous capabilities should be considered for APIs such as report operations, batch operations, and cold data queries.

In addition, although GraphQL solves the problem of aggregating multiple HTTP requests into one request, the schema will recursively obtain all data in a layer-by-layer parsing method. For example, the total number of statistics for paging queries, the original query that can be done once, has evolved into N + 1 queries to the database. In addition, unreasonable writing can lead to poor performance problems, so we need to pay special attention in the design process.

6. Exception response and error mechanism

There has been a lot of debate in the industry about whether RPC APIs throw exceptions or throw error codes. "Alibaba Java Development Manual" recommends that the isSuccess() method, "error code", and "error brief message" should be used preferentially for cross-application RPC calls. The reasons for using the Result method as the return method of the RPC method: 1) If the caller does not catch the return method by throwing an exception, a runtime error will occur. 2) If you don't add stack information, just create a new custom exception and add an error message of your own understanding, it will not help the caller to solve the problem too much. If stack information is added, in the case of frequent call errors, the performance loss of data serialization and transmission is also a problem. Of course, I also support the practical proponents of this argument.

public ResultXxxDTO> getXxx(String param) {
    try {
        // ...
        return Result.create(xxxDTO);
    } catch (BizException e) {
        log.error("...", e);
        return Result.createErrorResult(e.getErrorCode(), e.getErrorInfo(), true);
    }
}

During the Web API design process, we use ControllerAdvice to uniformly wrap error messages. In the complex chain calls of microservices, it is more difficult for us to track and locate the problem than the monolithic architecture. Therefore, special attention is required when designing. A better solution is to use the global exception structure response information when a non-2xx HTTP error code response occurs on the RESTful API interface. Among them, the code field is used to indicate the error code of a certain type of error, and the "{biz_name}/" prefix should be added to the microservice to facilitate locating which business system the error occurred on. Let's look at a case. Suppose that an interface in the "User Center" does not have permission to obtain resources and an error occurs. Our business system can respond to "UC/AUTH_DENIED" and obtain it in the log system through the request_id field of the automatically generated UUID value. Error details.

HTTP/1.1 400 Bad Request
Content-Type: application/json
{
   "code": "INVALID_ARGUMENT",
   "message": "{error message}",
   "cause": "{cause message}",
   "request_id": "01234567-89ab-cdef-0123-456789abcdef",
   "host_id": "{server identity}",
   "server_time": "2014-01-01T12:00:00Z"
}

7. Thinking about the idempotency of APIs

The core of the idempotent mechanism is to ensure the uniqueness of resources. For example, repeated submissions by the client or multiple retries by the server will only produce one result. Payment scenarios, refund scenarios, and transactions involving money cannot have multiple deductions and other issues. In fact, the query interface is used to obtain resources, because it only queries data and does not affect the changes of resources, so no matter how many times the interface is called, the resources will not change, so it is idempotent. The newly added interface is non-idempotent, because calling the interface multiple times will cause resource changes. Therefore, we need to be idempotent in the presence of duplicate commits. So, how to guarantee the idempotent mechanism? In fact, we have many implementations. One of the solutions is the common creation of unique indexes. Creating a unique index in the database for the resource fields we need to constrain can prevent duplicate data from being inserted. However, in the case of sub-database and sub-table, the unique index is not so easy to use. At this time, we can query the database once, and then judge whether the constrained resource fields are duplicated, and then perform the insert operation when there is no duplicate. . Note that, in order to avoid concurrent scenarios, we can ensure the uniqueness of data through locking mechanisms, such as pessimistic locking and optimistic locking. Here, distributed locking is a frequently used scheme, which is usually an implementation of pessimistic locking. However, many people often regard pessimistic locks, optimistic locks, and distributed locks as solutions for idempotent mechanisms, which is incorrect. In addition, we can also introduce a state machine, and use the state machine to perform state constraints and state jumps to ensure the process execution of the same business, thereby realizing data idempotence. In fact, not all interfaces are guaranteed to be idempotent. In other words, whether an idempotent mechanism is required can be determined by considering whether it is necessary to ensure resource uniqueness. For example, behavior logs may not consider idempotency. Of course, there is another design scheme that the interface does not consider the idempotent mechanism, but is guaranteed at the business level when the business is implemented. For example, multiple copies of data are allowed, but the latest version is obtained for processing during business processing.

(End, reprint please indicate the author and source.)

write at the end

[Server-side thinking]: Let's talk about the core technology of the server side, and discuss the project structure and practical experience of the first-line Internet. At the same time, the big family of "back-end circle" with many technical experts is looking forward to your joining, a group of people with the same frequency, grow together, improve together, and break the limitations of cognition.

More exciting articles, all in "Server Thinking"!

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324188394&siteId=291194637