
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.
1public interface IGitHubApi
2{
3 [Get("/users/{username}/repos")]
4 Task<List<Repository>> GetUserRepositoriesAsync(string username);
5}
6
7// Creating a Refit client
8IGitHubApi gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");
9
10// Using the client to make a request
11List<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.
1public interface IMyHttpApiClient
2{
3 [Get("/users/{user}")]
4 Task<ApiResponse<UserResponseItem>> GetUser(string user);
5}
6
7public 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).
1public class MyApiProvider(IMyHttpApiClient Client)
2{
3 public Task<UserResponseItem> GetUser(string user)
4 {
5 return Execute(() => Client.GetUser(user));
6 }
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.
1 private async Task<T> Execute<T>(Func<Task<ApiResponse<T>>> func)
2 {
3 ApiResponse<T> response;
4
5 try
6 {
7 response = await func.Invoke().ConfigureAwait(false);
8 }
9 catch (ApiException ex)
10 {
11 throw MapException(ex);
12 }
13
14 return response.Content;
15 }
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.
1public class MyApiException(ApiException ApiException) : Exception;
2public class MyApiForbiddenException(ApiException ApiException) : MyApiException(ApiException);
3public class MyApiServerErrorException(ApiException ApiException) : MyApiException(ApiException);
These can be declared and fired with the help of a simple mapping:
1 private static MyApiException MapException(ApiException ex)
2 {
3 return ex.StatusCode switch
4 {
5 System.Net.HttpStatusCode.InternalServerError => new MyApiServerErrorException(ex),
6 System.Net.HttpStatusCode.Forbidden => new MyApiForbiddenException(ex),
7 // more cases..
8 _ => new MyApiException(ex),
9 };
10 }
Full Sample
1using System;
2using System.Threading.Tasks;
3using Refit;
4
5namespace BenjaminAbt.RefitApiCallSample;
6
7// api models
8public record class UserResponseItem(Guid Id, string Name);
9
10// refit contract represents a client
11public interface IMyHttpApiClient
12{
13 [Get("/users/{user}")]
14 Task<ApiResponse<UserResponseItem>> GetUser(string user);
15}
16
17// custom call wrapper = glue code
18public class MyApiProvider(IMyHttpApiClient Client)
19{
20 public Task<UserResponseItem> GetUser(string user)
21 {
22 return Execute(() => Client.GetUser(user));
23 }
24
25 private async Task<T> Execute<T>(Func<Task<ApiResponse<T>>> func)
26 {
27 ApiResponse<T> response;
28
29 try
30 {
31 response = await func.Invoke().ConfigureAwait(false);
32 }
33 catch (ApiException ex)
34 {
35 throw MapException(ex);
36 }
37
38 return response.Content;
39 }
40
41 private static MyApiException MapException(ApiException ex)
42 {
43 return ex.StatusCode switch
44 {
45 System.Net.HttpStatusCode.InternalServerError => new MyApiServerErrorException(ex),
46 System.Net.HttpStatusCode.Forbidden => new MyApiForbiddenException(ex),
47 // more cases..
48 _ => new MyApiException(ex),
49 };
50 }
51}
52
53// Custom Exceptions
54public class MyApiException(ApiException ApiException) : Exception;
55public class MyApiForbiddenException(ApiException ApiException) : MyApiException(ApiException);
56public 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.

Comments