DEV Community

Cover image for Refit in .NET: Building Robust API Clients in C#
Milan Jovanović
Milan Jovanović

Posted on • Originally published at milanjovanovic.tech on

Refit in .NET: Building Robust API Clients in C#

As a .NET developer, I've spent countless hours working with external APIs. It's a crucial part of modern software development, but let's be honest - it can be a real pain sometimes.

We've all been there, wrestling with HttpClient, writing repetitive code, and hoping we didn't miss a parameter or header somewhere.

That's why I want to introduce you to Refit , a library that's been a game-changer for me.

Imagine turning your API into a live interface - sounds too good to be true, right? But that's exactly what Refit does. It handles all the HTTP heavy lifting, letting you focus on what matters: your application logic.

In this article, I'll explain how Refit can transform the way you work with APIs in your .NET projects.

What is Refit?

Refit is a type-safe REST library for .NET. It allows you to define your API as an interface, which Refit then implements for you. This approach reduces boilerplate code and makes your API calls more readable and maintainable.

You describe your API endpoints using method signatures and attributes, and Refit takes care of the rest.

Let me break down why I find Refit so powerful:

  • Automatic serialization and deserialization: You won't have to convert your objects to JSON and back. Refit handles all of that for you.
  • Strongly-typed API definitions: Refit helps you catch errors early. If you mistype a parameter or use the wrong data type, you'll know at compile time, not when your app crashes in production.
  • Support for various HTTP methods: GET, POST, PUT, PATCH, DELETE - Refit has you covered.
  • Request/response manipulations: You can add custom headers or handle specific content types in a straightforward way.

But what I appreciate most about Refit is how it promotes clean, readable code. Your API calls become self-documenting. Anyone reading your code can quickly understand what each method does without diving into implementation details.

Setting Up and Using Refit in Your Project

Let's set up Refit and see it in action using the JSONPlaceholder API. We'll implement a full CRUD interface and demonstrate its usage in a Minimal API application.

First, install the required NuGet packages:

Install-Package Refit
Install-Package Refit.HttpClientFactory
Enter fullscreen mode Exit fullscreen mode

Now, let's create our Refit interface:

using Refit;

public interface IBlogApi
{
    [Get("/posts/{id}")]
    Task<Post> GetPostAsync(int id);

    [Get("/posts")]
    Task<List<Post>> GetPostsAsync();

    [Post("/posts")]
    Task<Post> CreatePostAsync([Body] Post post);

    [Put("/posts/{id}")]
    Task<Post> UpdatePostAsync(int id, [Body] Post post);

    [Delete("/posts/{id}")]
    Task DeletePostAsync(int id);
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
    public int UserId { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

We define our IBlogApi interface with methods for all CRUD operations: GET (single and list), POST, PUT, and DELETE. The Post class represents the structure of our blog posts.

Then you have to register Refit in your dependency injection container:

using Refit;

builder.Services
    .AddRefitClient<IBlogApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"));
Enter fullscreen mode Exit fullscreen mode

Finally, we can use IBlogApi in our Minimal API endpoints:

app.MapGet("/posts/{id}", async (int id, IBlogApi api) =>
    await api.GetPostAsync(id));

app.MapGet("/posts", async (IBlogApi api) =>
    await api.GetPostsAsync());

app.MapPost("/posts", async ([FromBody] Post post, IBlogApi api) =>
    await api.CreatePostAsync(post));

app.MapPut("/posts/{id}", async (int id, [FromBody] Post post, IBlogApi api) =>
    await api.UpdatePostAsync(id, post));

app.MapDelete("/posts/{id}", async (int id, IBlogApi api) =>
    await api.DeletePostAsync(id));
Enter fullscreen mode Exit fullscreen mode

What I love about this setup is its simplicity. We've created a fully functional API that communicates with an external service, all in just a few lines of code. No manual HTTP requests, no raw JSON handling - Refit takes care of all that for us.

Query Parameters and Route Binding

When working with APIs, you often need to send data as part of the URL, either in the route or as query parameters. Refit makes this process simple and type-safe.

Let's extend our IBlogApi interface with some more complex scenarios:

public interface IBlogApi
{
    // Other methods omitted for brevity

    [Get("/posts")]
    Task<List<Post>> GetPostsAsync([Query] PostQueryParameters parameters);

    [Get("/users/{userId}/posts")]
    Task<List<Post>> GetUserPostsAsync(int userId);
}

public class PostQueryParameters
{
    public int? UserId { get; set; }
    public string? Title { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Let's break this down:

  • GetPostsAsync uses an object to represent query parameters. This approach is excellent for endpoints with many optional parameters. Refit will automatically convert this object into a query string.
  • GetUserPostsAsync demonstrates passing in route parameters (userId) directly.

Using an object for query parameters makes your code type-safe and refactoring-friendly. If you need to add a new query parameter, you just add a property to PostQueryParameters. Your existing code won't break, and your IDE can help you discover the new options.

Dynamic Headers and Authentication

Another common requirement when integrating with APIs is including custom headers or authentication tokens with your requests. Refit provides several ways to handle this, from simple static headers to dynamic, request-specific authentication.

Let's explore some scenarios:

public interface IBlogApi
{
    [Headers("User-Agent: MyAwesomeApp/1.0")]
    [Get("/posts")]
    Task<List<Post>> GetPostsAsync();

    [Get("/secure-posts")]
    Task<List<Post>> GetSecurePostsAsync([Header("Authorization")] string bearerToken);

    [Get("/user-posts")]
    Task<List<Post>> GetUserPostsAsync([Authorize(scheme: "Bearer")] string token);
}
Enter fullscreen mode Exit fullscreen mode
  • You can add a static header to all requests by using the Headers attribute
  • With the Header attribute, you can pass a header value dynamically as a parameter
  • The Authorize attribute is a convenient way to add Bearer token authentication

But what if you need to add the same dynamic header to all requests?

That's where DelegatingHandler comes in handy.

You can learn more about using delegating handlers in this article.

I've found delegating handlers especially helpful in providing API keys, which are typically static.

JSON Serialization Options

Refit gives you flexibility when choosing and configuring your JSON serializer. By default, Refit uses System.Text.Json, is the built-in JSON serializer in modern .NET versions.

However, you can easily switch to Newtonsoft.Json if you need its features.

Here's how you can configure Refit to use it.

First, install the Newtonsoft.Json support package:

Install-Package Refit.Newtonsoft.Json
Enter fullscreen mode Exit fullscreen mode

Then, configure Refit to use Newtonsoft.Json:

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Refit;

builder.Services.AddRefitClient<IBlogApi>(new RefitSettings
{
    ContentSerializer = new NewtonsoftJsonContentSerializer(new JsonSerializerSettings
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
        NullValueHandling = NullValueHandling.Ignore
    })
})
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"));
Enter fullscreen mode Exit fullscreen mode

This setup uses camel case for property names and ignores null values when serializing.

System.Text.Json is faster and uses less memory, making it a great default choice. However, Newtonsoft.Json offers more features and might be necessary for compatibility with older systems or specific serialization needs.

Handling HTTP Responses

While Refit's default behavior of automatically deserializing responses into your defined types is convenient, there are times when you need more control over the HTTP response.

Refit provides two options for these scenarios:HttpResponseMessage and ApiResponse<T>.

Let's update the IBlogApi to use these types:

public interface IBlogApi
{
    [Get("/posts/{id}")]
    Task<HttpResponseMessage> GetPostRawAsync(int id);

    [Get("/posts/{id}")]
    Task<ApiResponse<Post>> GetPostWithMetadataAsync(int id);

    [Post("/posts")]
    Task<ApiResponse<Post>> CreatePostAsync([Body] Post post);
}
Enter fullscreen mode Exit fullscreen mode

Now, let's break down how to use these, starting with HttpResponseMessage:

HttpResponseMessage response = await blogApi.GetPostRawAsync(1);

if (response.IsSuccessStatusCode)
{
    var content = await response.Content.ReadAsStringAsync();
    var post = JsonSerializer.Deserialize<Post>(content);
    Console.WriteLine($"Retrieved post: {post.Title}");
}
else
{
    Console.WriteLine($"Error: {response.StatusCode}");
}
Enter fullscreen mode Exit fullscreen mode

This approach gives you full control over the HTTP response. You can access status codes, headers, and the raw content. But you will have to deal with deserialization manually.

ApiResponse<T> is a Refit-specific type that wraps the deserialized content and response metadata. It's a great middle ground when you need the typed response and access to headers or status codes.

Here's a more complex example using ApiResponse<T> for creating a post:

var newPost = new Post { Title = "New Post", Body = "Content", UserId = 1 };

ApiResponse<Post> createResponse = await blogApi.CreatePostAsync(newPost);

if (createResponse.IsSuccessStatusCode)
{
    var createdPost = createResponse.Content;
    var locationHeader = createResponse.Headers.Location;
    Console.WriteLine($"Created post with ID: {createdPost.Id}");
    Console.WriteLine($"Location: {locationHeader}");
}
else
{
    Console.WriteLine($"Error: {createResponse.Error.Content}");
    Console.WriteLine($"Status: {createResponse.StatusCode}");
}
Enter fullscreen mode Exit fullscreen mode

This approach allows you to access the created resource, check specific headers like Location, and handle errors gracefully.

Takeaway

Refit transforms the way we interact with APIs in .NET applications. Converting your API into a strongly typed interface simplifies your code, enhances type safety, and improves maintainability.

The key Refit benefits we've explored include:

  • Simplified API calls with automatic serialization and deserialization
  • Flexible parameter handling for complex queries
  • Easy management of headers and authentication
  • Options for JSON serialization to fit your project's needs
  • Granular control over HTTP responses when required

In my experience, Refit shines in projects of all sizes. I've used it in small applications and large-scale microservices architectures. It eliminates boilerplate code, reduces the risk of errors, and allows you to focus on your application's core logic rather than the intricacies of HTTP communication.

Remember, while Refit makes API interactions more straightforward, it's not a substitute for understanding the underlying principles of RESTful communication and HTTP.

That's all for today. Stay awesome, and I'll see you next week.

P.S. You can find the source code for this example in this repository.


P.S. Whenever you're ready, there are 3 ways I can help you:

  1. Pragmatic Clean Architecture: Join 3,050+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.

  2. Modular Monolith Architecture: Join 950+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.

  3. Patreon Community: Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

Top comments (0)