Skip to main content
2 of 2
added 121 characters in body
Stilgar
  • 1.5k
  • 2
  • 10
  • 13

How to design a statically typed REST API client?

Last time I used I created a REST API client in .NET I used exceptions to represent status codes which don't indicate success. (404 was returning null). It's been quite some time since then and my ideas have evolved a bit. Now I am using Blazor for part of my project and I am faced with the problem of API client design. I am specifically interested in the response object design. C# 8.0 provides pattern matching which opens some new options for this design.

My API returns the object serialized as JSON if the call is executed correctly, 500 if internal server error, 404 if the object is not found, 400 if the validation fails, 401 and 403 in the corresponding authentication and authorization cases and the interesting part 409 Conflict when the error is one that should be displayed to the user. An example of such an error might be a registration call where the email is found to exist in the database or trying to post a comment without confirming the email. These are basically errors that would require additional API calls to validate on the client. So my question is how do I design a statically typed API client in C# 8.0 which allows me to express as much intent in the method signature and is easy to call and handle. 404 become nullable types but what about the rest? As I see it I have a few options

  • Do it the old school way and make a bunch of exceptions for the other cases. This means that exceptions will be effectively used for flow control as I will have to use try/catch to branch. Even 500 errors are handled in an app with UI. For example if you get a 500 you display something like "Unexpected error" to the user and need to unlock the form that was locked during the post. This is different from server side programming where you would just bubble the exception to some global handler, craft an error response and terminate the request. You can't just kill the client app if one request fails (or maybe you can?). I can think of some workarounds here like a message bus that handles the exception and displays the message in an universal way but what about unlocking the form and what about these 409 responses which should result in the message from the server being displayed to the user?

  • Craft response objects. The ASP.NET Identity has examples of this like the IdentityResult class which has a bool Succeeded property and an Errors property. You are supposed to if-check the Succeeded property and use the Errors property if it is false. This pattern can be extended to have a Result property which can be examined if the request needs to return some object. What I don't like about this approach is that you can forget the check. You can get this result even if you didn't check the property.

  • My final idea is to create Result types designed to be used with pattern matching. For example like this

public abstract class Result
{

}

public sealed class Success<T> : Result
{
    public T Value { get; }

    public Success(T value)
    {
        Value = value;
    }
}

public sealed class Error : Result
{
    public string Message { get; }

    public Error(string message)
    {
        Message = message;
    }
}

public sealed class ExpectedError : Result
{
    public string Message { get; }

    public ExpectedError(string message)
    {
        Message = message;
    }
}

They would be used like this

Result userResult = await apiClient.RegisterUserAsync(something);

if (userResult is Success<User> success)
{
   User user = success.Value;
   //do something
}
else if (userResult is ExpectedError expectedError)
{
   DisplayError(ExpectedError.Message);
}
else
{
    DisplayError("Something went wrong");
}

This will at least force the client code to check if the request was executed successfully although it will not force handling the errors. I've also seen libraries that do similar thing with methods like OnSuccess, anyone has experience with these?

So which approach would you recommend? How can these approaches be improved? Any other options I have not considered? Maybe exceptions are fine and I can add some form of global handlers on the Blazor side that will keep the form's state?

Stilgar
  • 1.5k
  • 2
  • 10
  • 13