REST & Open API Specification with Azure Function App

Summary

This page summarises key concepts that should be considered when designing REST APIs. The concepts are derived from the Mozilla HTTP including various other resources.

HTTP Methods

The common HTTP methods used by most RESTful web APIs are:

  • GET retrieves a representation of the resource at the specified URI. The body of the response message contains the details of the requested resource.
  • POST creates a new resource at the specified URI. The body of the request message provides the details of the new resource. Note that POST can also be used to trigger operations that don’t actually create resources.
  • PUT either creates or replaces the resource at the specified URI. The body of the request message specifies the resource to be created or updated.
  • PATCH performs a partial update of a resource. The request body specifies the set of changes to apply to the resource.
  • DELETE removes the resource at the specified URI.

REST API Design Best Practices

  1. REST APIs are designed around resources, https://eax360.com/api/customer, where the Customer is the resource.
  2. Resources have identifiers, https://eax360.com/api/customer/001, where the 001 is the Customer’s unique record.
  3. Clients interact with a service by exchanging representations of resources. Many web APIs use JSON as the exchange format.
  4. REST APIs built on HTTP use verbs to perform operations on resources, the most common operations are GET, POST, PUT, PATCH, and DELETE.
  5. REST APIs use a stateless request model. HTTP requests should be independent and may occur in any order, so keeping transient state information between requests is not feasible.
  6. A REST API Maturity model exists. The maturity model can be found here.
  7. Adopt a consistent naming convention in URIs. 
  8. Adopt a consistent taxonomy between resources. For example; https://eax360.com/api/customer/001/orders/ would result in returning all the orders for the 001 Customer. However, do not overcomplicate the resource URL.
  9. Avoid designing “chatty” APIs that expose a large number of small resources. Denormalize the data as you would when designing for performance.
  10. Avoid introducing dependencies between the web API and the underlying data sources. 

For more information read the specification details on the Mozilla.org website: Mozilla HTTP Reference.

POST, PUT & PATCH Differences

A POST request creates a resource. The server assigns a URI for the new resource and returns that URI to the client.

A PUT request creates a resource or updates an existing resource. The client specifies the URI for the resource. The request body contains a complete representation of the resource. If a resource with this URI already exists, it is replaced. Otherwise, a new resource is created. PUT requests are not supported by Web Browsers.

A PATCH request performs a partial update to an existing resource. The client specifies the URI for the resource. The request body specifies a set of changes to apply to the resource. This can be more efficient than using PUT, because the client only sends the changes, not the entire representation of the resource.

Media Types & Headers

In the HTTP protocol, formats are specified through the use of media types, also called MIME types. For non-binary data, most HTTP APIs support JSON (media type = application/json) and possibly XML (media type = application/xml). A complete list of media types can be found here.

HTTP Codes

The full set of HTTP Request codes can be found on the following page:

GET Request

The GET method is used to retrieve a resource. A successful GET method returns a HTTP status code 200 (OK). If the resource cannot be found, the method should return 404 (Not Found). If the calling application sends a bad request then a 500 is returned.

GET: https://eax360.com/api/customer
GET: https://eax360.com/api/customer/{resource_id}
[FunctionName("GetAllCustomers")]
[OpenApiOperation(operationId: "GetAllCustomers", tags: new[] { "Customer API" }, Summary = "Retrieve all Customer records", Description = "This API provides access to all the Customer records in the database system.")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(string), Description = "The operation completed successfully")]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.BadRequest, Description = "The operation was not completed successfully")]
public async Task<IActionResult> GetAllCustomers([HttpTrigger(AuthorizationLevel.Function, "get", Route = "customer")] HttpRequest req)
{
    try
    {
        // Business logic

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

        dynamic data = JsonConvert.DeserializeObject(requestBody);

        string responseMessage = "Returned all Customer records";

        return new OkObjectResult(responseMessage);
    }
    catch (Exception e)
    {
        _logger.LogInformation(e.ToString());
        return new OkObjectResult(HttpStatusCode.BadRequest);
    }
}
[FunctionName("GetCustomerById")]
[OpenApiOperation(operationId: "GetCustomerById", tags: new[] { "Customer API" }, Summary = "Retrieve a Customer record by ID", Description = "This API provides returns a Customer record by ID")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiParameter(name: "id", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "The **Id** parameter")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(string), Description = "The operation completed successfully")]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.BadRequest, Description = "The operation was not completed successfully")]
public async Task<IActionResult> GetCustomerById([HttpTrigger(AuthorizationLevel.Function, "get", Route = "customer/{id}")] HttpRequest req, string id)
{
    try
    {
        // Business logic

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

        dynamic data = JsonConvert.DeserializeObject(requestBody);

        string responseMessage = "Returned Customer by ID";

        return new OkObjectResult(responseMessage);
    }
    catch (Exception e)
    {
        _logger.LogInformation(e.ToString());
        return new OkObjectResult(HttpStatusCode.BadRequest);
    }
}

POST Request

As mentioned above, a POST request is used to create a new resource.

  1. If a POST method creates a new resource, it should return a HTTP status code 201 (Created). The URI of the new resource should also be included in the Location header of the response. The response body should contain a representation of the resource.
  2. If the POST method does some processing but does not create a new resource, the method should return a HTTP status code 200 and include the result of the operation in the response body. Alternatively, if there is no result to return, the method can return HTTP status code 204 (No Content) with no response body.
  3. If the client puts invalid data into the request, the server should return HTTP status code 400 (Bad Request). The response body can contain additional information about the error or a link to a URI that provides more details.
  4. If the client puts a valid date, but the request fails due to an error on the server-side, the HTTP status code 500 (Internal Server Error) should be returned.
  5. POST requests need to be authorised by tokens (see here and here).
POST: https://eax360.com/api/customer
[FunctionName("PostCustomer")]
[OpenApiOperation(operationId: "PostCustomer", tags: new[] { "Customer API" }, Summary = "Add a new Customer record", Description = "This API provides access to create a new Customer in the database system.")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiParameter(name: "id", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "The **Id** parameter")]
[OpenApiRequestBody("application/json", typeof(Customer), Description = "JSON request body containing { hours, capacity}")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(string), Description = "The operation completed successfully")]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.BadRequest, Description = "The operation was not completed successfully")]
public async Task<IActionResult> PostCustomer([HttpTrigger(AuthorizationLevel.Function, "post", Route = "customer")] HttpRequest req)
{
    try
    {
        // Business logic

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

        if (requestBody.Length > 2)
        {
            string responseMessage = "Returned all Customer records";

            return new OkObjectResult(responseMessage);
        }
        else
        {
            return new OkObjectResult(HttpStatusCode.BadRequest);
        }
    }
    catch (Exception e)
    {
        _logger.LogInformation(e.ToString());
        return new OkObjectResult(HttpStatusCode.BadRequest);
    }
}

Calling the API with JavaScript Fetch

var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

var raw = JSON.stringify({
  "name": "syed hussain",
  "contact": "syed@acme.com"
});

var requestOptions = {
  method: 'POST',
  headers: myHeaders,
  body: raw,
  redirect: 'follow'
};

fetch("https://eax360.com/api/customer", requestOptions)
  .then(response => response.text())
  .then(result => console.log(result))
  .catch(error => console.log('error', error));

PUT Request

  1. PUT is idempotent, invoking the function multiple times will not enforce duplicates.
  2. PUT method creates a new resource and returns HTTP status code 201 (Created), as with a POST method. If the method updates an existing resource, it returns either 200 (OK) or 204 (No Content).
  3. If it is not possible to update an existing resource, return a HTTP status code 409 (Conflict).
  4. Where possible, implement bulk HTTP PUT operations that can batch updates to multiple resources in a collection.
  5. The PUT request should specify the URI of the collection, and the request body should specify the details of the resources to be modified. This approach can help to reduce chattiness and improve performance.
PUT: https://eax360.com/api/customer/{resource_id}
[FunctionName("PutCustomerById")]
[OpenApiOperation(operationId: "PutCustomerById", tags: new[] { "Customer API" }, Summary = "Update a Customer record by ID", Description = "This API provides access to update a new Customer in the database system.")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiParameter(name: "id", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "The **Id** parameter")]
[OpenApiRequestBody("application/json", typeof(Customer), Description = "JSON request body containing { hours, capacity}")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(string), Description = "The operation completed successfully")]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.BadRequest, Description = "The operation was not completed successfully")]
public async Task<IActionResult> PutCustomerById([HttpTrigger(AuthorizationLevel.Function, "put", Route = "customer/{id}")] HttpRequest req, string id)
{
    try
    {
        // Business logic

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

        if (requestBody.Length > 2)
        {
            string responseMessage = "Returned all Customer records";

            return new OkObjectResult(responseMessage);
        }
        else
        {
            return new OkObjectResult(HttpStatusCode.BadRequest);
        }
    }
    catch (Exception e)
    {
        _logger.LogInformation(e.ToString());
        return new OkObjectResult(HttpStatusCode.BadRequest);
    }
}

PATCH Request

With a PATCH request, the client sends a set of updates to an existing resource, in the form of a patch document. The server processes the patch document to perform the update. The patch document doesn’t describe the whole resource, only a set of changes to apply. The specification for the PATCH method (RFC 5789) doesn’t define a particular format for patch documents. The format must be inferred from the media type in the request.

JSON is probably the most common data format for web APIs. There are two main JSON-based patch formats, called JSON patch and JSON merge patch.

JSON merge patch is somewhat simpler. The patch document has the same structure as the original JSON resource but includes just the subset of fields that should be changed or added. In addition, a field can be deleted by specifying null for the field value in the patch document. (That means a merge patch is not suitable if the original resource can have explicit null values.)

PATCH /file.txt HTTP/1.1
Host: eax360.com/resource/
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 100

[description of changes]

DELETE Request

  1. If the DELETE operation is successful, the web server should respond with HTTP status code 204 (No Content), indicating that the process has been successfully handled, but that the response body contains no further information.
  2. If the resource doesn’t exist, the webserver can return HTTP 404 (Not Found).
DELETE: https://eax360.com/api/customer/{resource_id}
[FunctionName("DeleteCustomerById")]
[OpenApiOperation(operationId: "DeleteCustomerById", tags: new[] { "Customer API" }, Summary = "Delete a Customer record by ID", Description = "This API provides access to delete a new Customer in the database system.")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiParameter(name: "id", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "The **Id** parameter")]
[OpenApiRequestBody("application/json", typeof(Customer), Description = "JSON request body containing { hours, capacity}")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(string), Description = "The OK response")]
public async Task<IActionResult> DeleteCustomerById([HttpTrigger(AuthorizationLevel.Function, "delete", Route = "customer/{id}")] HttpRequest req, string id)
{
    try
    {
        // Business logic

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

        if (requestBody.Length > 2)
        {
            string responseMessage = "Returned all Customer records";

            return new OkObjectResult(responseMessage);
        }
        else
        {
            return new OkObjectResult(HttpStatusCode.BadRequest);
        }
    }
    catch (Exception e)
    {
        _logger.LogInformation(e.ToString());
        return new OkObjectResult(HttpStatusCode.BadRequest);
    }
}