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:

  1. 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.
  2. APIs must not use the database sequence number as the resource identifier.
  3. A UUID, Hashed Id (HMAC based) is preferred as a resource identifier.
  4. 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.
  5. Enumeration values can be used as sub-resource IDs. String representation of the enumeration value should be used.
  6. There must not be two resource identifiers one after the other. Example: https://api.foo.com/v1/payments/payments/12345/102030
  7. Resource IDs should try to use either Resource Identifier Characters or ASCII characters. There should not be any ID using UTF-8 characters.
  8. 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.
  9. 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/ image

Each API, internal or external should have documentation published on this resource.

4.1. Design flow

  1. Determine what types of resources an API provides.
  2. Determine the relationships between resources.
  3. Decide the resource name schemes based on types and relationships.
  4. Decide the resource schemas.
  5. 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


  1. Web API Design
  2. https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
  3. Creating World-Class Developer Experiences
  4. Error handling RFC 7807 - Problem Details for HTTP APIs
  5. REST API Design Rulebook
  6. The Design of Web APIs
  7. Design Guidelines
  8. https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md
  9. https://github.com/paypal/api-standards/blob/master/api-style-guide.md
  10. Understanding Idempotency and Safety in API Design
  11. https://en.wikipedia.org/wiki/HATEOAS
  12. Fielding Dissertation: CHAPTER 5: Representational State Transfer (REST)
  13. https://semver.org/

Updated: