Inflection API Development Guidelines
1. Paradigm
REST is a selected paradigm for the API definitions across all Inflection north-south oriented services. GraphQL and gRPC are being evaluated for the east-west traffic.
This section covers recommendations for defining REST interface.
1.1. Use nouns instead of verbs in endpoint paths
REST best practices expect “verbiage” to come in the form of HTTP verbs (GET, PUT, POST, PATCH etc.), and not as a part of the URI. URI should contain only nouns, whenever possible. This isn’t a REST constraint, but it’s considered the best possible implementation, and allows developers to keep things consistent.
The HTTP verbs are suitable for most of the needs program may have. Oftentimes, if it feels like “need more verbs” in order to successfully implement a feature. It is a sign that thinking of the resource on which that call is being made needs to be expanded.
For example, if there is a login functionality should be implemented for the user, most tempting is to create the /login endpoint, which has a verb in it. Instead, the same functionality can be introduced in a RESTful way - by introducing ‘session’ entity which is tied to the user:
/users/1234/session/
NOT
/users/1234/login
1.2. Use hyphens in URIs
Use hyphen for word separation, instead of underscores or camel case:
/code-quality
NOT
/codeQuality
NOT
/code_quality
1.3. Stick to plurals
When naming your resource, use plural nouns for the collections.
/dogs
NOT
/dog
However, if there is only one dog available by this uri, e.g. there is no potential to go further after the last ‘/’ (like /dogs/123) - name it singular.
1.4. Simplify associations
Avoid deep nesting of the resources. Once you have the primary key for one level, you usually don’t need to include the levels above because you’ve already got your specific object. In other words, you shouldn’t need too many cases where a URL is deeper than what we have above
/resource/identifier/resource:
/owners/1234
/veterinarians/1234
/dogs/1234
/food/1234
NOT
/owners/1234/veterinarians/565/dogs/784/food/1247
It is acceptable to have sub-resources exist directly beneath an individual resource in cases, when it is an entity, which cannot exist without its parent entity and has a compound key - so cannot be uniquely identified globally:
/state/CA/county/Alameda
However, if you have or foresee the need of querying subresources - put it into the root:
/counties?populationGreaterThan=10000
So the recommendation is to avoid subresources, especially for sub-collections.
1.5. Use filter parameters
Make it simple for developers to use the base URL by putting optional states and attributes behind the HTTP question mark. To get all red dogs running in the park:
/dogs?color=red&state=running&location=park
NOT
/red-dogs-running-in-the-park
1.6. HTTP verbs usage and idempotency
Operations must use the proper HTTP methods whenever possible, and operation idempotency must be respected.
Operation is idempotent if it produces the same result when called over and over. Idempotency is an important aspect of building a fault-tolerant API. Idempotent APIs enable clients to safely retry an operation without worrying about the side-effects that the operation can cause. For example, a client can safely retry an idempotent request in the event of a request failing due to a network connection error. Following table shows some common HTTP verbiage, and whether or not it is idempotent.
Method | Description | Is Idempotent |
---|---|---|
GET | Return the current value of an object | True |
PUT | Replace an object, or create a named object, when applicable | True |
DELETE | Delete an object | True |
POST | Create a new object based on the data provided, or submit a command | False |
HEAD | Return metadata of an object for a GET response. Resources that support the GET method may support the HEAD method as well | True |
PATCH | Apply a partial update to an object | False |
OPTIONS | Get information about a request | True |
1.6.1. GET
The GET method must not have side effects. It must not change the state of an underlying resource.
1.6.2. POST
POST operations should support the Location response header to specify the location of any created resource that was not explicitly named, via the Location header.
As an example, imagine a service that allows creation of hosted servers, which will be named by the service: POST http://api.contoso.com/account1/servers The response would be something like: 201 Created Location: http://api.contoso.com/account1/servers/server321 Where “server321” is the service-allocated server name. Services may also return the full metadata for the created item in the response. POST operation may be made idempotent, by accepting the idempotency key (see Idempotency key pattern on Stripe API). The idempotency key that is supplied as part of every POST request must be unique and can not be reused with another request with a different input payload.
1.6.3. PATCH
PATCH has been standardized by IETF as the method to be used for updating an existing object incrementally (see RFC 5789).
1.6.4. DELETE
DELETE operation should return the same code no matter if it changed the state of the system or not.
1.7. Resource Identifiers
Resource Identifiers identify a resource or a sub-resource. These must conform to the following guidelines:
- The lifecycle of a resource identifier must be owned by the resource’s domain model, where they can be guaranteed to uniquely identify a single resource.
- APIs must not use the database sequence number as the resource identifier.
- A UUID, Hashed Id (HMAC based) is preferred as a resource identifier.
- For security and data integrity reasons all sub-resource IDs must be
scoped within the parent resource only.
Example: /users/1234/linked-accounts/ABCD
Even if account “ABCD” exists, it must NOT be returned unless it is linked to user: 1234. - Enumeration values can be used as sub-resource IDs. String representation of the enumeration value should be used.
- There must not be two resource identifiers one after the other. Example: https://api.foo.com/v1/payments/payments/12345/102030
- Resource IDs should try to use either Resource Identifier Characters or ASCII characters. There should not be any ID using UTF-8 characters.
- Resource IDs and query parameter values must perform URI percent-encoding for any character other than URI unreserved characters. Query parameter values using UTF-8 characters must be encoded.
- Personally identifiable information (PII) parameters must not be accepted in the URL (as part of path or query string) because this information can be inadvertently exposed via client, network, server logs and other mechanisms.
1.8 Input format
1.8.1. Use camelCase in models
Only json is supported as an input/output format. All field names should be represented in camel case.
1.8.2. Datetimes
The date and time string must conform to the date-time universal format defined in section 5.6 of RFC3339.
All APIs must only emit UTC time (aka Zulu time or GMT) in the responses.
When processing requests, an API should accept date-time or time fields that contain an offset from UTC. For example, 2016-09-28T18:30:41.000+05:00 SHOULD be accepted as equivalent to 2016-09-28T13:30:41.000Z. Offset should only be used to calculate the equivalent UTC time before it is persisted in the system[13] (because of known platform/language/DB interoperability issues). A UTC offset must not be used to derive anything else.
If the business logic requires expressing the timezone of an event, it is RECOMMENDED that you capture the timezone explicitly by using a separate request/response field. You SHOULD NOT use offset to derive the timezone information. The offset alone is insufficient to accurately transform the stored UTC time back to a local time later. The reason is that a UTC offset might be same for many geographical regions and based on the time of the year there may be additional factors such as daylight savings. For example, an offset UTC-05:00 represents Eastern Standard Time during winter, Central Daylight Time during summer, and year-round offset for Panama, Columbia, and Peru.
The timezone string MUST be per IANA timezone database (aka Olson database or tzdata or zoneinfo database), for example America/Los_Angeles for Pacific Time, or Europe/Berlin for Central European Time.
When expressing floating time values that are not tied to specific time zones such as user’s date of birth, expiry date, publication date etc. in requests or responses, an API SHOULD NOT associate it with a timezone. The reason is that a UTC offset changes the meaning of a floating time value. For examples, all countries with timezones west of prime meridian would consider a floating time value to be the previous day.
1.8.3. Enums
Enums should be represented as pascal cased strings, and not as numbers.
1.9. Handling errors
For nonsuccess conditions, developers should be able to write one piece of code that handles errors consistently across different services. This allows building of simple and reliable infrastructure to handle exceptions as a separate flow from successful responses.
See RFC 7807. Shared error response structure is to be defined.
Supported HTTP status codes:
Code | Description |
---|---|
200 OK | Successful |
201 Created | Resource created |
202 Accepted | The request has been accepted for processing, but the processing has not been completed |
400 Bad Request | Bad input parameter. Error message should indicate which one and why |
401 Unauthorized | The client passed in the invalid authentication info |
403 Forbidden | The client has insufficient permissions to access the resource |
404 Not Found | Resource not found |
405 Method Not Allowed | The resource doesn’t support the specified HTTP verb |
409 Conflict | Conflict |
429 Too Many Requests | Too many request for rate limiting |
500 Internal Server Error | Servers are not working as expected. The request is probably valid but needs to be requested again later |
503 Service Unavailable | Service Unavailable |
2. Versioning policy
API evolution is our primary versioning approach. This means that we won’t have an explicit version number in the URL and won’t make any incompatible changes.
Following API evolution will allow us to have minimal duplication, legacy build up, and complexities which come with supporting multiple versions. Thus will yield to the less cost of API support and development.
Over time, we may develop implicit versioning similar to Stripe’s if we have a use case where we absolutely need to support multiple versions.
2.1 API versioning
API should not have version number embedded in the url, and should look like api.inflection.com/searches.
Based on experience with other Inflection APIs, it is unlikely that we’ll need to release v2 of the API due to drastic domain changes.
However, modeling mistakes may happen. To eliminate them, each API should pass the review and dog-fooding phase internally to ensure that the domain is modeled correctly in the API interface.
2.2 Endpoints versioning
As said above, evolution is the primary versioning approach. In case we need to introduce the breaking change, we need to do this in a backward compatible manner with a clear deprecation timeline.
Example of API evolution in practice:
Case #1: change the field name + type in the request model: from
{
"report" : "report name"
}
to
{
"reports" : [
"report name 1",
"report name 2"
]
}
Introduce new “reports” field in addition to “report“:
{
"report" : "report name",
"reports" : [
"report name 1",
"report name 2"
]
}
Set deprecation date for the “report” field and communicate it to all consumers. Drop the “report” field after communicated deprecation date.
Case #2: reduce amount of data returned in the response model from
{
"reports" : [
{
"search": "SSN trace",
"status": "alert",
"data": "report data"
}
]
}
to
{
"reports" : [
{
"search": "SSN trace",
"status": "alert"
}
]
}
Set deprecation date for the “data” field and communicate it to all consumers.
Drop the “data” field after communicated deprecation date.
Case #3: split the resource from api.inflection.com/reports to api.inflection.com/candidates and api.inflection.com/searches Create a new set of endpoints replacing /reports resource.
Set deprecation date for /reports resource and communicate it to all consumers.
Drop the /reports resource after communicated deprecation date.
2.3 Definition of a breaking change
Changes to the contract of an API are considered a breaking change. Changes that impact the backwards compatibility of an API are a breaking change.
Clear examples of breaking changes:
- Removing or renaming APIs or API parameters
- Changes in behavior for an existing API
- Changes in Error Codes and Fault Contracts
- Anything that would violate the Principle of Least Astonishment
Services must explicitly define their definition of a breaking change, especially with regard to adding new fields to JSON responses and adding new API arguments with default fields. Services that are co-located behind a DNS Endpoint with other services must be consistent in defining contract extensibility. The applicable changes described in this section of the OData V4 spec should be considered part of the minimum bar that all services must consider a breaking change.
3. Deprecation policy
3.1. Announcements
All effort should be taken to notify consumers, through all relevant communication channels of new deprecations.
3.1.1 Internal consumers
Jira ticket(s) should be created to accommodate changes within all Inflection code. Communicate with the code owner, either apply changes yourself and ask for the review and help with testing, or make sure it’s prioritized within a reasonable timeframe.
Additionally, an announcement about deprecation should be made via engineering communication channel, to give a heads-up to people who potentially are in the middle of integrating with the affected endpoints.
3.1.2 External consumers
Send an email to all partners who have active API accounts for the api.inflection.com describing changes and the deprecation date. Email format will be attached to the API guidelines.
Before dropping the endpoint, check the logs whether requests are still coming to it, and communicate deprecation additionally with the request originators if there are any.
4. Documentation
Documentation is hosted on https://inflection.stoplight.io/
Each API, internal or external should have documentation published on this resource.
4.1. Design flow
- Determine what types of resources an API provides.
- Determine the relationships between resources.
- Decide the resource name schemes based on types and relationships.
- Decide the resource schemas.
- Attach a minimum set of methods to resources.
4.2. Maximum output for minimum input
Service should be developed with an intention of taking the minimum required information and extracting the maximum value of it.
For example, if there is a dog and the owner who always live at the same place, but input model has both owner’s and dog’s addresses - this can be simplified by introducing shared ‘Home’ field:
{
"Owner": {
"Name": "John"
},
"Dog": {
"Name": "Travis"
},
"Home": "555 Twin Dolphin Drive"
}
NOT
{
"Owner": {
"Name": "John",
"Home": "555 Twin Dolphin Drive"
},
"Dog": {
"Name": "Travis",
"Home": "555 Twin Dolphin Drive"
}
}
4.3. Externalizable
Service must be designed so that the functionality it provides is easily externalizable.
A service is developed for use by consumers that may be from another domain or team, another business unit or another company. In all of these cases, the functionality exposed is the same; what changes is the access mechanism or the policies enforced by a service, like authentication, authorization and rate-limiting. Since the functionality exposed is the same, the service should be designed once and then externalized based on business needs through appropriate policies.
This principle implies the following:
- The service interface must be derived from the domain model and the intended use-cases it is meant to support
- The service contract and access (binding) protocols supported must meet the consumer’s needs
- The externalization of a service must not require
reimplementation, or a change in service contract. Example: given an endpoint returning all paid credit verification
requests, which was initially created for the usage on Goodhire.
GET /creditVerifications?isPaid=true
Later on, new requirements came in to start using this endpoint on SafeDecision too. So instead of adding isSafeDecision=true filter parameter (even if you know there are only two business domains ATM):
GET /creditVerifications?isGoodhire=true&isPaid=true
make it more generic, so it’s not biased to any of the domains:
GET /creditVerification?tenant=GoodHire&isPaid=true
References
- Web API Design
- https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
- Creating World-Class Developer Experiences
- Error handling RFC 7807 - Problem Details for HTTP APIs
- REST API Design Rulebook
- The Design of Web APIs
- Design Guidelines
- https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md
- https://github.com/paypal/api-standards/blob/master/api-style-guide.md
- Understanding Idempotency and Safety in API Design
- https://en.wikipedia.org/wiki/HATEOAS
- Fielding Dissertation: CHAPTER 5: Representational State Transfer (REST)
- https://semver.org/