Rules to Better Web API - gRPC

​​

Hold on a second! How would you like to view this content?
Just the title! A brief blurb! Gimme everything!
  1. Do you know when to use gRPC?

    gRPC is an API protocol that uses binary (rather than JSON) data, leading to lightweight payloads and better performance.

    ​​Check out this video from Liam Elliott to learn more about gRPC and when you can use it in your solutions.

     



  2. Do you return detailed error messages?

    Good error design is as important to the sucess of an API as the API design itself. A good error message provides context and visibility on how to troubleshoot and resolve issues at critical times.

    For REST, start by using the correct HTTP Status Codes...

    The HTTP/1.1 RFC lists over 70 different HTTP Status Codes. Very few developers will be able to remember all of them, so it pays to keep it simple and use the most common Status Codes. The basic rule is to use the following three:

    • 200 OK - Everything worked. Success
    • 400 Bad Request - The consuming application did something wrong.
    • 500 Internal Server Error - The API Application did something wrong.

    ...And then include the problem details

    RFC 7807 - Problem Details for HTTP APIs (ietf.org) details the specification for returning errors from your API. The HTTP Status Codes are an excellent start - they immediately tell you where the problem is, but they don't tell you what the problem is.
     

    ASP.Net Core has built in support for the problem details specification. You can see more at the official documentation. Handle errors in ASP.NET Core web APIs | Microsoft Docs

    And for any API:

    Make your error messages as verbose as necessary...

    Error messages should contain a sufficient level of information that a developer or consuming client can act upon.

    {
        "errorMessage": "An error has occurred."
    }

    Figure: Bad Example - The error message does not contain information that can be acted upon.

    {
        "errorMessage": "Client ID is a required field. Please provide a Client ID."
    }

    Figure: Good Example - The error message provides explicit detail and a short description on how to fix the issue.

    ...But no more verbose than that

    ​​HTTP/1.1 500 Internal Server Error
    Transfer-Encoding: chunked
    Content-Type: text/plain
    Server: Microsoft-IIS/10.0
    X-Powered-By: ASP.NET
    Date: Fri, 27 Sep 2019 16:13:16 GMT

    System.ArgumentException: We don't offer a weather forecast for chicago. (Parameter 'city')
       at WebApiSample.Controllers.WeatherForecastController.Get(String city) in C:\working_folder\aspnet\AspNetCore.Docs\aspnetcore\web-api\handle-errors\samples\3.x\Controllers\WeatherForecastController.cs:line 34
       at lambda_method(Closure , Object , Object[] )
       at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
       at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
       at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
       at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
       at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
       at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
       at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
    --- End of stack trace from previous location where exception was thrown ---
       at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
       at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
       at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
       at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
       at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

    HEADERS
    =======
    Accept: */*
    Host: localhost:44312
    User-Agent: curl/7.55.1​

    Figure: Bad Example - this level of data should not be returned in a production environment​

    Provide a Tracking or Correlation ID

    A tracking or correlation ID will allow the consuming clients to provide the API developers with a reference point in their logs.

    {
        "errorMessage": "An error has occurred. Please contact technical support"
    }

    Figure: Bad Example - No tracking or correlation ID is provided.

    {
        "errorMessage": "An error has occurred. Please contact technical support",
        "errorId": "3022af02-482e-4c06-885a-81d811ce9b34"
    }

    Figure: Good Exmaple - A error ID is provided as part of the reponse.

    Provide an additional Help Resource

    Providing a URI to an additional help resources as part of your request will allow consuming clients to find additional resources or documentation that relates to the defined problem. 

    {
      "ErrorType": "DoesNotExist",
      "Id": "3022af02-482e-4c06-885a-81d811ce9b34",
      "Message": "No Client with a ID of 999999999 was found",
      "StatusCode": 404
    }

    Figure: Bad Example - No Help Link Provided

    {
      "ErrorType": "DoesNotExist",
      "HelpLink": "http://www.myapiapplication/api/help/doesnotexist",
      "Id": "3022af02-482e-4c06-885a-81d811ce9b34",
      "Message": "No Client with a ID of 999999999 was found",
      "StatusCode": 404
    }

    Figure: Good Example - A help link is provided as part of the response.

  3. Do you return the correct response code?

    The use of correct response codes is a simple yet crucial step towards building a better WebAPI. In ASP.NET Core, by default the WebAPI framework sets the response status code to 200 (OK), regardless of whether the task succeed or an error occurred.  

    You can save yourself countless hours of painful debugging, by specifying the correct response code.

    For example: According to the HTTP/1.1 protocol, when a POST request results in the creation of a resource, the server should reply with status 201 (Created).

    public Product PostProduct(Product item)
    {
    item = repository.Add(item);
    return item;
    }

    Figure: Bad Example – By default a 200 status code is returned.

    [ResponseType(typeof(CreditSnapshot))]
    public HttpResponseMessage PostProduct(Product item)
    {
    item = repository.Add(item);
    var response = Request.CreateResponse(HttpStatusCode.Created, item);

    return response;
    }

    Figure: Good Example – When creating objects the “Created” status code is returned. 

    ​public void PutProduct(int id, Product product)
    {
    product.Id = id;
    if (!repository.Update(product))
    {
    return Request.CreateResponse(HttpStatusCode.NotFound, ex.Message);
    }
    }

    Figure: Good Example – When updating or deleting objects, if the object to be modified cannot be found throw exception with HttpStatusCode.NotFound
  4. Do you use Fluent Validation?

    ​Client-side validation provides a great user experience but this must always be backed up by server-side validation.

    cartoon-client-side-validation.jpg
    Figure: Client-side validation does not provide effective data security for your Web API endpoints

    .NET and .NET Core Web APIs provide built-in support for validation using Data Annotations:

    1. Decorate your model classes with validation attributes, e.g. [Required], [MaxLength(60)]
    2. The MVC data binding system will automatically validate all entities sent to a controller and set ModelState.IsValid and ModelState.Values / Errors
    3. As per Do You Apply the ValidateModel Attribute to All Controllers?  you can create an attribute to apply this validation to all your Web API endpoints

    Fluent Validation improves the built-in capabilities in a number of ways:

    1. It is outside of your ApiController, so can be shared with other API protocols (like GraphQL or gRPC).
    2. It plugs directly into the existing data binding and validation engine (as above) so you can adopt Fluent Validation without changing the client side
    3. It is also easy to apply Fluent Validation to inner layers of your application
    4. You can specify multiple rulesets for a model without modifying the model itself
    5. Fluent validation uses a powerful Fluent API with LINQ expressions

      using FluentValidation;

      public class CustomerValidator: AbstractValidator<Customer> {
        public CustomerValidator() {
          RuleFor(x => x.Surname).NotEmpty();
          RuleFor(x => x.Forename).NotEmpty().WithMessage("Please specify a first name");
          RuleFor(x => x.Discount).NotEqual(0).When(x => x.HasDiscount);
          RuleFor(x => x.Address).Length(20, 250);
          RuleFor(x => x.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
        }

        private bool BeAValidPostcode(string postcode) {
          // custom postcode validating logic goes here
        }
      }

      Good example: Fluent Validation uses LINQ expressions allowing the development of powerful, type-checked rulesets without needing to modify the class under validation. 
    6. You can write conditional rules with the .When clause. This is great for complex form validation.

          RuleFor(x => x.Discount).NotEqual(0).When(x => x.HasDiscount);

      Good Example: Conditional validation with the .When() clause allows for complex logic such as “Discount number cannot be 0 if the HasDiscount boolean is true” 
    7. Fluent Validation provides a great entry-point for writing your own custom, complex rules.

      For most modern Web APIs the response type is usually JSON. The validation errors raised by Fluent Validation serialize easily to JSON making it fairly trivial to handle these errors from whatever client-side framework you are using.

      {
        "CompanyName": [
          "The CompanyName field is required."
        ]
      }

      Good Example: This is the JSON returned from Fluent Validation when a validation rule fails. This is exactly the same format as what would be returned by the built-in ModelState validation.