Refit .NET - my personal caller best practise

Refit .NET - my personal caller best practise

Refit is an open-source library for .NET developers that simplifies the process of making HTTP API calls. It allows developers to define a set of interfaces that describe the API endpoints they want to interact with, and then Refit automatically generates the necessary code to make the HTTP requests. This can significantly reduce boilerplate code and make the interaction with web APIs more type-safe and maintainable.

public interface IGitHubApi
{
    [Get("/users/{username}/repos")]
    Task<List<Repository>> GetUserRepositoriesAsync(string username);
}

// Creating a Refit client
IGitHubApi gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");

// Using the client to make a request
List<Repository> repositories = await gitHubApi.GetUserRepositoriesAsync("octocat");

In this example, IGitHubApi is an interface that defines a single API endpoint to get a user's repositories from GitHub. The [Get] attribute specifies that this is a GET request and defines the endpoint's URL template. Refit then generates the implementation for this interface, allowing the developer to make the API call with gitHubApi.GetUserRepositoriesAsync("octocat").

Overall, Refit is a powerful tool for .NET developers looking to streamline their interactions with HTTP APIs, offering a clean, declarative approach to API client generation.

My personal best practise

Very simple but important: the definition of a refit interface corresponds to a client in the architecture; the interface should therefore also be defined as a client.

public interface IMyHttpApiClient
{
    [Get("/users/{user}")]
    Task<ApiResponse<UserResponseItem>> GetUser(string user);
}

public record class UserResponseItem(Guid Id, string Name);

As can already be seen here, this also includes a model for the response, which must also be named in accordance with the naming rules for DTOs. The same, if you had request models.

The main focus of my personal best practice is how the call itself is handled - and in the rarest of cases, the simple, direct use of the client (interface) is the best and most sustainable option; on the contrary. As a rule, things like authentication or error handling are added, which should be encapsulated in a separate wrapper (a provider).

public class MyApiProvider(IMyHttpApiClient Client)
{
    public Task<UserResponseItem> GetUser(string user)
    {
        return Execute(() => Client.GetUser(user));
    }

The execution is now centralized - the Execute method - so that each API call can also be handled centrally. This would also be the right place to integrate authentication, monitoring, tracing, metrics or logging.

    private async Task<T> Execute<T>(Func<Task<ApiResponse<T>>> func)
    {
        ApiResponse<T> response;

        try
        {
            response = await func.Invoke().ConfigureAwait(false);
        }
        catch (ApiException ex)
        {
            throw MapException(ex);
        }

        return response.Content;
    }

You should create your own exceptions for exception handling in order to make yourself independent of the Refit implementation; Refit does have its own exception (ApiException), but this is not necessarily suitable or intended to make itself dependent on its content, but only to use it as a source.

public class MyApiException(ApiException ApiException) : Exception;
public class MyApiForbiddenException(ApiException ApiException) : MyApiException(ApiException);
public class MyApiServerErrorException(ApiException ApiException) : MyApiException(ApiException);

These can be declared and fired with the help of a simple mapping:

    private static MyApiException MapException(ApiException ex)
    {
        return ex.StatusCode switch
        {
            System.Net.HttpStatusCode.InternalServerError => new MyApiServerErrorException(ex),
            System.Net.HttpStatusCode.Forbidden => new MyApiForbiddenException(ex),
            // more cases..
            _ => new MyApiException(ex),
        };
    }

Full Sample

using System;
using System.Threading.Tasks;
using Refit;

namespace BenjaminAbt.RefitApiCallSample;

// api models
public record class UserResponseItem(Guid Id, string Name);

// refit contract represents a client
public interface IMyHttpApiClient
{
    [Get("/users/{user}")]
    Task<ApiResponse<UserResponseItem>> GetUser(string user);
}

// custom call wrapper = glue code
public class MyApiProvider(IMyHttpApiClient Client)
{
    public Task<UserResponseItem> GetUser(string user)
    {
        return Execute(() => Client.GetUser(user));
    }

    private async Task<T> Execute<T>(Func<Task<ApiResponse<T>>> func)
    {
        ApiResponse<T> response;

        try
        {
            response = await func.Invoke().ConfigureAwait(false);
        }
        catch (ApiException ex)
        {
            throw MapException(ex);
        }

        return response.Content;
    }

    private static MyApiException MapException(ApiException ex)
    {
        return ex.StatusCode switch
        {
            System.Net.HttpStatusCode.InternalServerError => new MyApiServerErrorException(ex),
            System.Net.HttpStatusCode.Forbidden => new MyApiForbiddenException(ex),
            // more cases..
            _ => new MyApiException(ex),
        };
    }
}

// Custom Exceptions
public class MyApiException(ApiException ApiException) : Exception;
public class MyApiForbiddenException(ApiException ApiException) : MyApiException(ApiException);
public class MyApiServerErrorException(ApiException ApiException) : MyApiException(ApiException);

And in the end, that was a very simple but, in my opinion, very effective best practice with the central handling of Refit, which I use in this form in many projects, from small to very large ones.