preface
Some time last year, I wanted to write a joke about interface. At that time, the interface scheme proposed by the back end was very uncomfortable for me to call, but I couldn’t say why. There was no argument so I gave it up. Recently, the team encountered some internal interface design issues due to writing full stacks, and began to think about best practices for implementing interfaces. After referring to many materials, I gradually have my own understanding of this problem. At the same time recall the past experience, finally realized their pain point at that time in where.
Since this is a joke, please forgive my unfriendly attitude. All the examples in this article are from my own personal experience.
Who should take the lead in interface design
Or, to put it more bluntly, should the consumer or provider of the interface decide its design?
The consumer of the interface, of course
The most bizarre thing about the interface is that providers go to a lot of trouble to implement it, but it doesn’t (almost) use it. It’s easy to get into a situation where he doesn’t know the interface is good or bad. It’s like a cook who never tastes his own food. You can’t expect his food to be any better, his cooking to be any better. The underlying premise is (I think) that interfaces are absolutely good and bad, with bad interfaces being bad for consumers to invoke, bad for providers to maintain, and bad for product behavior and poor experience.
But what does a good interface have to do with who leads the design? Because one of the reasons for bad interfaces is that providers only solve problems from the developer’s perspective:
Example 1 (Chatty API)
At some point, you need to implement a feature that allows users to create dashboard pages (if you’re new to dashboard pages, you can imagine it as a single page with different charts, such as bar charts, line charts, pie charts, etc.). Users can add their own charts to the page and manually resize and position them. Dashboards are usually used to provide an overview of the operating status of a product or service. The preliminary interface design of the backend students is that after the user fills in the basic information, adds the chart and clicks the create button, I need to call the interface twice in a row to complete a dashboard creation:
- Create an empty dashboard with the basic information the user fills in and the size and location of the chart
- Fill the dashboard with specific information about the chart, such as the chart type, dimensions and metrics used
It is clear that he is designing the interface according to his back-end storage structure, not only the storage structure, but even the stored procedures. Imagine an extreme scenario where instead of just providing some interface for updating database tables, the front end inserts data into the library itself through the interface
With interfaces of this low-level nature, consumers need to consider the interface invocation steps and understand the rationale behind them when integrating. If the underlying structure of the back end changes, the interface will most likely need to change as well, and so will the calling code on the front end.
Backend developers might argue that the backend uses microservices. Different types of data are stored on different services, so you need to communicate with different services to achieve full storage. What they don’t get is that the backend implementation leads to fragmentation of the interface, and that’s your problem, rather than shifting that burden to the developers on the front end and, indirectly, to the users. Don’t bully me if I don’t understand the back end, at least I understand that adding a Layer of Orchestration similar to BFF can solve this problem
In The Future of API Design: The Orchestration Layer, Netflix engineer Daniel Jacobson points out that apis address two types of audience:
- LSUD: Large set of unknown developers
- SSKD: Small set of known developers
As products become servitized, it is likely that interfaces will need to be exposed to public developers, namely LSUD, as AWS or Github does. Leaving aside whether or not the interface scheme in the example above will be drowned in spitfire, it is dangerous to expose the details of the internal service so obviously.
So let the consumer take the lead when designing the interface. If the consumer fails to give good advice, then at least the provider should put itself in the consumer’s shoes when designing. Or, at least, would you be happy to use an interface you designed yourself?
Using back-end thinking to design interfaces is not just about URI design, but also about request parameters and return body structure:
Example 2
Suppose you now want an interface that requests bulk articles and returns the contents of multiple articles at the same time, including the contents of those articles, author information, comment information, and so on.
Ideally, we would like to return data on a per-article basis, such as
articles: [
{
id:,author: {},
comments: []}, {id:
author: {},
comments: []}]Copy the code
However, the result returned by the back end may be in entities:
{
articles: [].authors: [].comments: []}Copy the code
Comments contains the comments of different articles, and I must group by them with a field like articleId to separate out the comments belonging to different articles. Do the same for other entities and eventually manually splice them into the Articles data structures required by the front-end code
This is clearly the result of a backend library table relationship, which is not strictly anti-pattern, and normalize data is encouraged in Redux. But don’t return raw data if the front end doesn’t need it. For example, if I need to display a percentage of data on the page, unless the user needs to dynamically format the data, such as thousandths, decimals, or switching precision, etc., just return me the percentage string, not the raw floating-point data. The secondary processing of data by the front end will also bring interference to the investigation of problems. If any data needs to be processed by the front end, the investigation of all problems must be initiated from the front end, and then enter the back-end investigation process after the front end confirms no error, which will always occupy the manpower of the two ends and delay the investigation progress
About Meta Information
Example 3:
A back-end interface usually returns with meta information, which includes the status of the interface and the failure cause if the interface fails, facilitating debugging. The data structure of meta information provided by the back-end interface is as follows:
{
meta: {
code: 0.error: null.host: "127.0.0.1"
},
result: []}Copy the code
In my opinion, there are two problems with this data structure
Meta information contains independent status information
In the design of meta information interfaces that contain status codes, a default hiding logic is: The HTTP status code returned by the interface must be 200, and whether the data was successfully retrieved is determined by the custom status code in the meta (in other words, the interface you see above is actually “interface of interface”). Finally, in the front-end code, there is no need to judge whether the return is normal through HTTP code, just need to judge the meta. Code returned in the interface
** But who gives you confidence that the back-end interface will not hang? ! ** No matter how hard the back end keeps the interface, the front end still needs to determine whether the HTTP code is 200 and then whether meta. Code is as expected. It’s not about trust, it’s about the robustness of my program.
Since the interface is going to be judged twice anyway, why not merge meta. Code with HTTP code? Moreover, I need to maintain a custom code enumeration value locally, and I need to ensure synchronization with the backend. Which brings us to the next question:
Location where the meta information is stored
There’s nothing wrong with needing meta information, but we don’t need meta information that much. This is reflected in several aspects:
- Do we really need a field to display meta information parallel to the returned result?
- Do we need meta information for every request?
- Does meta information have to be in the meta field?
Take, for example, a failed request error message. The error message will only appear if the interface returns abnormally, but should we always reserve a field for it in the return body?
On the question of where meta information exists, I prefer to consolidate it into HTTP headers. For example, meta. Code could have been HTTP code instead, I don’t see the point of always ensuring 200 returns and custom codes
Other meta information can be passed through custom HTTP headers starting with X-. For example,
Github API information about frequency limits is placed in the HTTP Header:
Status: 200 OK
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1372700873
Copy the code
Design for today
Example 4
We need to design a query interface for the line chart of a certain indicator. The query is in days, that is to say, the interface will only return the query result of the specified date according to the query date. The returned data structure provided by the backend is as follows:
{
data: [{date: "2019-06-08".result: []}]}Copy the code
Although the requirement explicitly states that only one day’s query results will be returned, the back end decides to return me an array. The reason for his design is to prevent future requirements from changing and returning query results over multiple days.
This seems like a smart decision: “Look, I’ve covered a future need in anticipation!” “, but it’s really stupid: you did cover a need, but one that doesn’t exist now and isn’t likely to happen in the future; And if you really want to write future-proof code, there are thousands of requirements waiting to be implemented.
The problem is that no one knows if multi-day data will ever be allowed to be queried simultaneously, and even if it does, the data structure doesn’t have to be that way. The query requirements we face in data analytics are not linear from single to multiple, nor are they in other business areas.
As a result, you spend extra time implementing code that you don’t need, and the front-end needs to be implemented with such data structures. And in future maintenance, everyone who sees an array body will wonder why I need to wrap an array around a single result. Did I miss something? You have to put effort into verifying that it’s really possible to return more data. The API and code should be precise and express exactly what you want to achieve without ambiguity
One might say isn’t that just another layer of encapsulation? It doesn’t take a lot of work to achieve it. Sorry, I am not specific to this case, but to emphasize that no matter how difficult it is to implement in any scenario, meaningless code should not be added. “Do not take evil for small” is the truth
There is another dimension to being present:
Examples of five
Now that we have an interface for creating individual articles, we need to support batch creation of articles. The advice from the back end is: how about tuning a single interface multiple times?
Example 6
There is already an interface for retrieving data about articles, such as content, comments, authors, and so on. Now we need to add a new page to display user information. The advice from the back end is to use the article data interface, which already contains the author information, so you don’t have to develop a new interface
The above examples seem to be trying to reuse the interface, but in fact they are less effective
In Example 5, while “create five articles” and “create one article five times in a row” are semantically equivalent, this is not the case at the implementation and operational level. Not to mention the big difference in performance between calling five times and calling once, five articles created in batches may have a sequential relationship and may require transactional operations.
In example 6, we can achieve the effect of our implementation, but this is not the reuse of the interface, only the interface hack (the difference between hack and reuse is whether to use the original function of the object to do things). In addition, hack interface is risky. For the interface provider, they are more concerned about the “orthodox” consumers of interface service. In this case, the existence of interface is to display complete article information. Shrink or even cancel fields. So they have no obligation to these hacks. An interface is supposed to focus on one thing
So the ideal thing is to develop a separate interface for your current business focus. In the example of Example 6, it might be possible to develop an interface that requests author information independently and implement code completely copied from the implementation of another interface, but the isolation of interfaces makes it easier to maintain functionality in the long run
It’s not limited to REST apis
“Interface” is a concept. We have a lot of options under the concept of how to implement it. So far, most of this has been done through REST apis, and there’s nothing that REST apis can’t do, but the fact that technology has evolved over the last few years has given us more options. What if we chose a more targeted implementation
For example, in the case of real-time data, it is theoretically the back end (when there is data update) that drives the update of the front view, which is supposed to be a push operation. But in traditional implementations, we still have to implement functionality through passive waiting and polling.
Using WebSocket or Streaming seems to be a better choice for event-driven needs. You can also use Webhooks for back-end interactions. I’m generally skeptical of new technologies, but I have to admit that GraphQL can handle some requirements better than REST apis. And most vendors’ support for the GraphQL interface indicates that it is feasible.
I understand that the implementation API is only a small part of the back-end implementation function, behind the interface is more business logic changes and changes in library table structure. Even half of the interface is handed over to the framework. However, even if there is only a small chance, it should be done perfectly.
conclusion
I could go on and on complaining about bad interface design, but it suddenly seemed pointless to keep writing about it. To be honest, I’m not here to complain, but interface design is critical. It is sad to see that many problems can be solved with some very basic skills, but everyone turns a blind eye to it and causes interlose-lose situation. These are the principles and considerations that I think should be followed in interface design. I believe that most of the pain points can be solved and some of the problems can be avoided
Back end students, if you want to make the interface better, listen to the “consumer” feedback. If you’ve ever tried to develop an app using third-party interfaces, such as Slack or Github, you’ll see that their interfaces are constantly iterative. Old interfaces are constantly being eliminated and new ones put into use. Behind this iteration is not idle idle work, but the voice and needs of actual users
Finally, I would like to recommend a book I read recently on API design, which is very profitable:
- Web API design and development
- Designing Web APIs
- APIs A Strategy Guide
This article is also published in my Zhihu column, welcome your attention