API Design Principle
In this article, I will only talk about WEB API in the microservice scenario, which is usually implemented by RESTFUL, RPC, etc.
Join the DZone community and get the full member experience.
Join For FreeAlthough API has more various definitions in a different scenario, in this blog, I will only talk about WEB API in micro-service scenario, which is usually implemented by RESTFUL, RPC, etc. API represents a capability of an instance of micro-service, hence the format of transition (like XML, JSON) has little impact in the design APIs
However, API design is crucial in the design of micro-service representing the method of communication between services and impacts the integration of services. Generally, a good API design should meet below 2 requirement
- Platform Independency. Each client can consume API without knowing how it is implemented. API should follow standard protocol and message format to provide service. Transition protocol and format should NOT invade the business logic, which means system should be able to support different transition protocol and message format.
- System Reliability. In the circumstance that API has been delivered and non-API version changes, API should be responsible to API contract, no any changes which might cause destructive data. When API needs tremendous update, the version update should reserve timeframe for older version.
In practice, API design is not an easy effort. Meanwhile, it is also difficult to evaluate the design. From system design and consumer perspective, here we list some simple design principle.
Use a Proven RESTful API
RESTful style API design has some nature advantage, for example, by using HTTP to reduce the coupling of clients and has very good openness. So more and more developers use RESTful style API. But RESTful is only an design idea rather than a principle due to lacking of constraints.
Hence, when we design RESTful API, we should refer to maturity model of RESTful.
Maturity Grade | Explanation | Example |
---|---|---|
Level 0 | Define a root API to finish all actions | POST /?action=changePassword |
Level 1 | Create isolated resource address and API scope | POST /user?action=update |
Level 2 | Use HTTP verb to define the action for resource | GET /users/001 |
Level 3 | Use API HATEOAS (Hypermedia as the Engine of Application State) to provide information dynamically | "links": { "deposit": "/accounts/12345/deposit", "withdraw": "/accounts/12345/withdraw", "transfer": "/accounts/12345/transfer", "close": "/accounts/12345/close" } |
According to scenario, we should choose suitable maturity grade of model.
Avoid Simple Encapsulation
API should be encapsulated for business, we should avoid to simply encapsulated API to became API for database throughout. For example, if an order status has been updated as "paid", we should provide the API like POST /orders/1/pay, not the API PATH /ORDERS/1 and update the order by param.
Because order payment has its detail business logic, and might involve large number of complicate actions, so using simple update method might leak the business logic out of system, and external system also needs to know internal "order status" field.
More importantly, it also destroys encapsulation of business logic and also impacts other functionality requirement such as privilege control, logs or notification.
Separation Concern
A good API should be neither more nor less. For example, user password change and user info change look very similar. However if we design a generic API /users/1/update URI, and define an "User" object
xxxxxxxxxx
{
"username": "xxxx"
"password": "xxxx"
}
When we use this object to change the user information, password is not necessary. But in action of changing password, one "password" filed is not enough, and "confirmPassword" might also be needed.
Then the API becomes
xxxxxxxxxx
{
"username": "xxxx"
"password": "xxxxx"
"confirmPassword": "xxxxx"
}
Such reuse will confuse the maintenance developers and is also not friendly to consumers. A reasonable design of API should break it into 2 separate API:
xxxxxxxxxx
//POST /users/{userId}/password
{
"password": "xxxxx"
"confirmPassword": "xxxxx"
}
//PATCH /users/{userId}
{
"username": "xxxx"
"other info": "xxxxx"
}
Accordingly, 2 DTO are created to handle them. It also shows separation of concern idea in Object-oriented.
MECE Principle
API should follow MECE principle (mutually exclusive and collective exhaustive", APIs should not be override for each other. Taking order and order item 2 resources as examples, If API: PUT orders/1/order-item/1 is designed to modify the order item, then API: PUT /orders/1 should not be capable to modify one order-item.
The benefit of the design is there are no overlay API which might cause complex on maintenance and understanding. But how to follow MECE principle?
The simple way is to design a table listing APIs and business capability.
Resource URI design coming from Domain Driven Design is very simple, aggregation root is root URL and entity is second URI. And Each aggregation root should have no any relation, responsibility for entity and aggregation root should also be clear.
Version
A service serving outside probably is changing all the time. Any business change might also change API parameters, response structure and relations among resources. Generally, new fields will not impact legacy client running. But if there are some destructive modification, then new version will be used to lead data to new resource address.
Version information could be transferred in below ways
- URI prefix
- Header
- Query
URI prefix is most suggested way, like /v1/users/ means the API fetching users list in version 1.
Common anti-pattern way is to use URI postfix to tell version like /users/1/updatev2. The drawback of this way is version is embedded into business logic, and cause inconvenient management of router.
Using Header and Using Query are similar. The difference is in MVC framework, using URI prefix is easier to be implemented by routing. But addition implementation of interceptor is needed to support Header or Query solution.
Naming
Naming in API design involves some aspects such as, URI, request parameter, response data, etc. Usually it is most difficult and most important to have a global consistent naming mechanism.
Secondly, naming needs cover below consideration:
- Try best to keep consistence of domain naming, for example aggregate root, entity, event, etc.
- Noun plurals should be used in URI in RESTful design
- Try not to use abbreviation like calling user to usr.
- Try to use character which needn't encoding
If multiple words existing in URI, "-" could be used to separate them like /orders/1/order-items
Security
Security is always most critical in any software design. From API design perspective, internal API is slight different with external API exposed to outside.
As an internal system, what to consider more should be whether API is robust enough. It should have enough ability to verify the receiving data and provide corresponding error message rather than receiving any data.
However there are more challenge for external APIs.
- Wrong call
- Interface abuse
- Illegal visit caused by browsers' security breach
Therefore, measures should be considered in API design. To handle those wrong call, API should respond error message before business flow; To handle interface abuse, some throttle limitation plans are needed; To handle browsers' security breach, some security related header should be added in response like: X-XSS-Protection, Content-Security-Policy, etc.
Reviewing List of API Design
- Whether URI naming is consistent via aggregate root and entity.
- Whether URI naming uses noun plural and dash
- Whether URI naming uses lower case word
- Whether URI expose unnecessary information like /cgi-bin
- Whether URI rule is consistent
- Whether the capabilities resources provide are independent.
- Whether there are characters which need encoding
- Whether the parameters in request and response are no more and no less
- Whether IDs in resources are passed by PATH
- Whether authentication and authorization information is exposed in Query parameters
- Whether parameters contain rare abbreviation
- Whether field naming is consistent between request and response
- Whether there is meaningless object like {" data":{ }}
- Whether data structure agreed is broken when exception
- Whether status codes are suitable
- Whether type of media is suitable
- Whether singular and plural is consistent with data content
- Whether cache data are in the response header
- Whether there is version management
- Whether version information exists as URI prefix
- Whether API provides expiration time
- Whether API index is provided
- Whether authentication and authorization are executed
- Whether HTTPS is used
- Whether illegal parameters are verified
- Whether security headers are added
- Whether there is throttling limitation strategy
- Whether CORS is supported
- Whether ISO 8601 standard is applied in date format
- Whether there is unauthorized access
Opinions expressed by DZone contributors are their own.
Comments