Sample API specification developed in RAML for managing customers.
The API specification is designed to meet the following use cases:
- Allowing automated API consumers (for example, cron jobs or middleware applications) invoke APIs to fetch list of customers.
- An end-user such as customer service representative should be able to use a mobile device to manage customers using the APIs
- In current state, the API specification only deals with customers. We should be able to expand to other resources such as orders and products with relative ease.
To preview the API specification in your browser, please see here for raml2html output.
In this exercise, we will make the assumption that the API server is protected by an Authorization Server which will do the OAuth verifications.
To encrypt traffic between the API consumer and API server, HTTPS will be used in conjunction with TLS. This will prevent man-in-the-middle attacks, packet sniffing and other similar malicious attacks.
Using HTTPS with TLS 1.2 is important as the APIs are internet facing (as opposed to point-to-point in a data centre or within a WAN/LAN setup). This warrants all communications to be encrypted end-to-end. TLS 1.0 is currently more widely adopted, however major API vendors are slowly deprecating it in favour of TLS 1.2.
The APIs will be protected using OAuth 2.0. OAuth 2.0 is deemed more secure than other methods such as API keys and Basic Auth as user authentication credentials are not passed around in every request. Although, a downside is that implementing OAuth is a bit more cumbersome than simple API keys or Basic Auth.
We will support two variants of OAuth - two-legged (application authentication) and three-legged (user authentication) and both are intended to satisfy the use cases 1 and 2 above.
Application authentication (or two-legged OAuth) does not require any user context. The Authorization process involves:
- An API client issued with
client_id
andclient_secret
- An API Authorization server validating the request, issuing the access and refresh tokens (discussed below in more details)
API client will need to keep the client_id
and client_secret
encrypted for security.
This is suitable for applications or automated scripts that run at regular interval to fetch client data.
This is the traditional OAuth scenario (three-legged scenario) where the API Client is redirect to the Authorization server to enter their credentials. Upon successful authentication, the client receives the access and refresh tokens that will be used to communicate with the API server.
This is suitable for mobile or web application clients with an end-user involved.
Clients who obtain their token via Application Authentication will only have read-only access to the data. They can only invoke the safe HTTP methods such as GET which does not manipulate any resource.
In this section we describe how we can fulfil first two use cases.
An API consumer that wish to invoke customer APIs periodically will need to register with the API service in order to obtain client ID and client secret.
Once they have obtained that, the rest of the OAuth Authorization will follow as below:
Step 1: API consumer sends a request for token by sending the client_id
and client_secret
obtained during registration in an Authorization
header as Base64 encoded string.
POST /oauth/token HTTP/1.1
Host: api.samplehost.com
Authorization: Basic Base64(client_id:client_secret)
Step 2: The Authorization server will respond with the token:
{
"token_type": "bearer",
"access_token": "79c11a62df3d4d92c092314071941e4489c8f8e7",
"refresh_token": "0b12356dfbd0980ed3347f4acf6a824d06b7b8e3"
}
Step 3: The client can then use the access_token
for all subsequent API calls by specifying it in the Authorization
header.
GET /api/v1/customers?pageNo=0 HTTP/1.1
Host: api.samplehost.com
Authorization: Bearer 79c11a62df3d4d92c092314071941e4489c8f8e7
The
refresh_token
can be used to obtain a newaccess_token
when that expires.
Due to the high volume of data that can be retrieved by the API which can affect performance, we need to enforce pagination which will restrict the amount of data that can be fetched per API call.
This will help reduce load on the server as the API's consumer base scales up.
This means that the API client will need to read the pagination attributes such as pageSize
, totalPages
, first
and last
to determine how many subsequent requests to make to extract the complete collection of the customer resource.
When automated API consumers will try to consume the API to fetch all customers, they will need to first perform an initial load of the full customer dataset. This will need to happen by iterating through all the pages of customer result set in one iteration. The API client will need to note the start timestamp of the first API call.
In all subsequent API calls (scheduled at regular interval or otherwise), the API consumer will only want records that have been updated since the last time the entire list was loaded. So after the initial data load, the API consumer will need to pass the lastUpdated
query parameter with the value since the last time the API call was made. This will return those customer resources that have been updated since the last time API call was made.
The ability to restrict result sets of collections by using lastUpdated
query parameter and pagination features allow us to prevent network and server overload by expensive API calls to retrieve collections.
Mobile and IoT devices should leverage the User Authentication
(three-legged OAuth) which will give them access to full range of APIs that the API service has to offer.
All mobile applications will need a client_id
and client_secret
which they will obtain after registering their mobile device or application with the API service.
End-users such as a Customer Service Representative using the application, will need to authenticate in the Authorization server which will issue the the end-user's device with the access token.
The authentication flow will be as follows:
- A customer service agent attempts to login in the mobile application.
- The mobile application redirects end-user to the Authorization Server which will present a page/screen for customer service agent to enter their username and password.
- Customer service agent will enter their own login credentials and upon submission, the Authorization Server will redirect back to the client application URL (Authorization grant process)
- The mobile application will extract the
authorization_code
from the response and call/oauth2/authorize
to obtain theaccess_token
andrefresh_token
- The Authorization server will return the
access_token
andrefresh_token
back to the mobile application. - The mobile application will use
access_token
for all future interactions with the API Server.
The API uses standard response model which look like below:
{
"success": true,
"status": 200,
"data": {
// actual response data goes here
}
}
We use this object for all HTTP responses - whether its 200 OK or 400 Bad Request or any other response types.
This way, application developers can easily write cleaner client-side code as they can always expect a single standard 3 attribute response object as response.
The API spec has been developed to make it easier further expand it to include other resources such as orders
and products
.
In the API spec, we defined two resource type called collectionType and resourceType.
collectionType is designed to be used on a collection (of resources). For example, /customers
or /accounts
or /products
.
resourceType is applicable to individual resource - a single item in the collection.
For example, /customers/{customerId}
, orders/{orderId}
.
The applicable HTTP methods (GET, PATCH, PUT, POST, DELETE) has been applied directly to the resource types instead of the resource themselves.
collectionType
allows the following method:
- GET - Retrieve the whole collection of resources
- POST - Create a new resource in the collection
resourceType
allows the following methods:
- GET - Retrieve resource
- PUT - Update resource
- DELETE - Delete resource
This way, we can simply add new resources and associate them with the resource types and it will cover the common API methods automatically.
Note: There may be cases were we may not want to allow a certain API operation on a collection or resource. For example, deleting customers might not be desirable, in this cases we would remove the HTTP DELETE method away from the resource type and apply it on the API definitions of the resource themselves.
In real life, we might not put a destructive operation like DELETE under resource type but rather directly on the collection or resources in the API definitions.
Suppose we are adding the following new resources: orders
and products
. An use case around these new resources is that customer
places orders
and orders
will contain number of products
. So there is a one-to-many between customers
and one-to-many between orders
and products
.
Using these relationships, the API URLs might look like below:
/customers/{customerId}/orders
: All orders that belong to a customer/customers/{customerId}/orders/{orderId}
: Specific order that belong to a customer/customers/{customerId}/orders/{orderId}/products
: List of products in an order that customer placed/products
: A collection of all products/products/{productId}
: A particular product
The following snippet shows how we could introduce a new order
resource and nest it within the current customer
resource:
### Types
types:
customer:
description: Model for 'Customer' resource
schema: !include schemas/customer.schema
order:
description: Model for 'order' resource
schema: !include schema/order.schema
### API definition
/customers:
### content removed for brevity ###
/orders:
type: {
## Associate type 'collectionType' to orders
collectionType:
{
examplePaginatedResponseExample : !include examples/getCustomerOrders-example.json,
examplePaginatedResponseType: paginatedResponse,
createResourceRequestExample : !include examples/order-example.json,
createResourceRequestType: order,
createResourceResponseExample: !include examples/createOrderResponse-example.json,
createResourceResponseType: order,
createResourceBadRequestExample: !include examples/badRequestExample.json
}
}
/orders/{orderId}:
## Associate type 'resourceType' with individual order item
type: {
resourceType:
{
updateResourceRequestExample : !include examples/order-example.json,
updateResourceRequestType: order,
updateResourceResponseExample: !include examples/order-example.json,
updateResourceResponseType: order,
getSingleResourceExample: !include examples/order-example.json,
getSingleResourceType: order
}
}
Note: The JSON schemas and samples responses need to be created as first step before the above API setups can be added.
Therefore, by associating the API methods with the resource types, we make this easily scalable to other resources.
Strict adherence to REST standards mandates having hypermedia links and in the community there are strong proponents both for and against having hypermedia links in the response body.
I have deliberately left out hypermedia links, as they do not really serve any purpose as far as the use cases are concerned. On the other hand, they make API response more bloated.
Conditional requests allow API client to validate whether their cached copy of the customer resource or collection is valid or not. It makes use of the ETag
/If-None-Match
headers for single resource API calls.
API client will need to cache or store the ETag
header value of the HTTP 200 OK Response and pass that to subsequent GET /api/v1/customer/{customerId}
requests using a If-None-Match
header.
If the resource has not been modified, the API server will respond with a HTTP 204 Not Modified with empty body which means API client can use their cached copy.