DEV Community

Jairo Blanco
Jairo Blanco

Posted on

Refit: The Elegant REST Library for .NET That Will Change How You Write API Clients

If you've ever written an HTTP client in .NET, you know the ritual: create an HttpClient, build the URL, set headers, serialize the request body, call SendAsync, check the status code, deserialize the response, and handle errors — for every single endpoint. It's repetitive, error-prone, and buries your intent under a mountain of boilerplate.

Refit eliminates all of that. With Refit, you define your REST API as a C# interface, and Refit generates the implementation at runtime. That's it.


What Is Refit?

Refit is a type-safe REST client library for .NET, inspired by Square's Retrofit for Android. Developed by Paul Betts and maintained by the ReactiveUI organization, it turns REST API definitions into live, callable objects — no implementation required.

"Turn your REST API into a live interface."

It supports .NET 6+, .NET Framework 4.6.1+, Xamarin, and MAUI, making it a universal solution across the .NET ecosystem.


Installation

Install via NuGet:

dotnet add package Refit
Enter fullscreen mode Exit fullscreen mode

If you're using ASP.NET Core and want seamless HttpClientFactory integration:

dotnet add package Refit.HttpClientFactory
Enter fullscreen mode Exit fullscreen mode

Your First Refit Client

Let's say you're consuming the JSONPlaceholder API. Here's how you'd build a client with raw HttpClient:

// The old way — verbose and fragile
var client = new HttpClient { BaseAddress = new Uri("https://jsonplaceholder.typicode.com") };
var response = await client.GetAsync("/posts/1");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var post = JsonSerializer.Deserialize<Post>(json);
Enter fullscreen mode Exit fullscreen mode

Now here's the Refit way:

// 1. Define the interface
public interface IJsonPlaceholderApi
{
    [Get("/posts/{id}")]
    Task<Post> GetPostAsync(int id);
}

// 2. Create the client
var api = RestService.For<IJsonPlaceholderApi>("https://jsonplaceholder.typicode.com");

// 3. Call it
var post = await api.GetPostAsync(1);
Console.WriteLine(post.Title);
Enter fullscreen mode Exit fullscreen mode

That's the entire implementation. Clean, readable, and self-documenting.


Core Concepts

HTTP Method Attributes

Refit supports all standard HTTP verbs:

public interface IPostsApi
{
    [Get("/posts")]
    Task<List<Post>> GetAllPostsAsync();

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

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

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

    [Patch("/posts/{id}")]
    Task<Post> PatchPostAsync(int id, [Body] JsonPatchDocument<Post> patch);

    [Delete("/posts/{id}")]
    Task DeletePostAsync(int id);
}
Enter fullscreen mode Exit fullscreen mode

Query Parameters

Pass query string parameters as method arguments:

public interface ISearchApi
{
    // Generates: /search?query=refit&page=2&pageSize=10
    [Get("/search")]
    Task<SearchResults> SearchAsync(string query, int page = 1, int pageSize = 10);
}
Enter fullscreen mode Exit fullscreen mode

Use [AliasAs] to map parameter names to query string keys:

[Get("/users")]
Task<List<User>> GetUsersAsync([AliasAs("sort_by")] string sortBy, [AliasAs("order")] string order);
Enter fullscreen mode Exit fullscreen mode

Request Headers

Add headers at the interface, method, or parameter level:

// On the interface — applies to all methods
[Headers("User-Agent: MyApp/1.0", "Accept: application/json")]
public interface IMyApi
{
    // On a specific method
    [Headers("Cache-Control: no-cache")]
    [Get("/sensitive-data")]
    Task<SensitiveData> GetSensitiveDataAsync();

    // Dynamic header via parameter
    [Get("/user/profile")]
    Task<UserProfile> GetProfileAsync([Header("Authorization")] string authorization);
}
Enter fullscreen mode Exit fullscreen mode

Request Body

Serialize complex objects to JSON automatically:

public interface IBlogApi
{
    [Post("/articles")]
    Task<Article> CreateArticleAsync([Body] CreateArticleRequest request);
}

var newArticle = new CreateArticleRequest
{
    Title = "Getting Started with Refit",
    Content = "Refit makes REST easy...",
    Tags = ["dotnet", "api", "refit"]
};

var created = await api.CreateArticleAsync(newArticle);
Enter fullscreen mode Exit fullscreen mode

Need to send form data instead of JSON?

[Post("/oauth/token")]
Task<TokenResponse> GetTokenAsync([Body(BodySerializationMethod.UrlEncoded)] TokenRequest request);
Enter fullscreen mode Exit fullscreen mode

Working with IApiResponse<T>

By default, Refit throws an ApiException on non-success HTTP responses. But sometimes you want to inspect the response without throwing. Use IApiResponse<T>:

public interface IProductsApi
{
    [Get("/products/{id}")]
    Task<IApiResponse<Product>> GetProductAsync(int id);
}

var response = await api.GetProductAsync(42);

if (response.IsSuccessStatusCode)
{
    Console.WriteLine($"Product: {response.Content!.Name}");
}
else
{
    Console.WriteLine($"Error {response.StatusCode}: {response.Error?.Message}");
}
Enter fullscreen mode Exit fullscreen mode

You can also use ApiResponse<T> (concrete class) to access response headers:

var response = await api.GetProductAsync(42);
var etag = response.Headers.ETag?.Tag;
Enter fullscreen mode Exit fullscreen mode

Integration with HttpClientFactory (ASP.NET Core)

The recommended approach in ASP.NET Core is to use Refit.HttpClientFactory for proper HttpClient lifecycle management:

// Program.cs
builder.Services
    .AddRefitClient<IGitHubApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com"));
Enter fullscreen mode Exit fullscreen mode

Then inject it anywhere via DI:

public class GitHubService
{
    private readonly IGitHubApi _github;

    public GitHubService(IGitHubApi github)
    {
        _github = github;
    }

    public async Task<IEnumerable<Repository>> GetUserReposAsync(string username)
    {
        return await _github.GetRepositoriesAsync(username);
    }
}
Enter fullscreen mode Exit fullscreen mode

Authentication with DelegatingHandler

A clean way to add Bearer tokens is via a custom DelegatingHandler:

public class AuthHeaderHandler : DelegatingHandler
{
    private readonly ITokenService _tokenService;

    public AuthHeaderHandler(ITokenService tokenService)
    {
        _tokenService = tokenService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await _tokenService.GetTokenAsync();
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it with DI:

builder.Services.AddTransient<AuthHeaderHandler>();

builder.Services
    .AddRefitClient<ISecureApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
    .AddHttpMessageHandler<AuthHeaderHandler>();
Enter fullscreen mode Exit fullscreen mode

Custom Serialization Settings

Refit uses System.Text.Json by default. To customize settings:

var settings = new RefitSettings
{
    ContentSerializer = new SystemTextJsonContentSerializer(
        new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
            Converters = { new JsonStringEnumConverter() }
        })
};

var api = RestService.For<IMyApi>("https://api.example.com", settings);
Enter fullscreen mode Exit fullscreen mode

Prefer Newtonsoft.Json? Install Refit.Newtonsoft.Json and swap the serializer:

var settings = new RefitSettings
{
    ContentSerializer = new NewtonsoftJsonContentSerializer(
        new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        })
};
Enter fullscreen mode Exit fullscreen mode

Multipart / File Upload

Uploading files is just as simple:

public interface IFileApi
{
    [Multipart]
    [Post("/upload")]
    Task<UploadResult> UploadFileAsync(
        [AliasAs("file")] StreamPart file,
        [AliasAs("description")] string description);
}

// Usage
await using var stream = File.OpenRead("report.pdf");
var filePart = new StreamPart(stream, "report.pdf", "application/pdf");
var result = await api.UploadFileAsync(filePart, "Monthly Report");
Enter fullscreen mode Exit fullscreen mode

Cancellation Token Support

All methods support CancellationToken as an optional last parameter — no special attributes needed:

public interface ISlowApi
{
    [Get("/long-running-report")]
    Task<Report> GetReportAsync(CancellationToken cancellationToken = default);
}

// Cancel after 5 seconds
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var report = await api.GetReportAsync(cts.Token);
Enter fullscreen mode Exit fullscreen mode

Real-World Example: GitHub API Client

Here's a realistic, production-style Refit client for the GitHub API:

[Headers("User-Agent: MyGitHubApp", "Accept: application/vnd.github.v3+json")]
public interface IGitHubApi
{
    [Get("/users/{username}")]
    Task<GitHubUser> GetUserAsync(string username);

    [Get("/users/{username}/repos")]
    Task<List<Repository>> GetRepositoriesAsync(
        string username,
        [AliasAs("sort")] string sort = "updated",
        [AliasAs("per_page")] int perPage = 30);

    [Get("/repos/{owner}/{repo}/issues")]
    Task<IApiResponse<List<Issue>>> GetIssuesAsync(
        string owner,
        string repo,
        [AliasAs("state")] string state = "open",
        CancellationToken cancellationToken = default);

    [Post("/repos/{owner}/{repo}/issues")]
    Task<Issue> CreateIssueAsync(
        string owner,
        string repo,
        [Body] CreateIssueRequest request,
        [Header("Authorization")] string authorization);
}
Enter fullscreen mode Exit fullscreen mode

Error Handling

Catch ApiException for structured error responses:

try
{
    var user = await api.GetUserAsync("nonexistent-user-xyz");
}
catch (ApiException ex)
{
    Console.WriteLine($"HTTP {ex.StatusCode}");
    Console.WriteLine($"Reason: {ex.ReasonPhrase}");

    // Deserialize the error body
    var error = await ex.GetContentAsAsync<GitHubError>();
    Console.WriteLine($"GitHub says: {error?.Message}");
}
Enter fullscreen mode Exit fullscreen mode

Testing Refit Interfaces

Since Refit generates implementations from interfaces, mocking is trivial with any mocking library:

// Using NSubstitute
var mockApi = Substitute.For<IPostsApi>();
mockApi.GetPostAsync(1).Returns(new Post { Id = 1, Title = "Test Post" });

var service = new PostService(mockApi);
var result = await service.GetPostTitleAsync(1);

Assert.Equal("Test Post", result);
Enter fullscreen mode Exit fullscreen mode

No complex HTTP mocking infrastructure needed — your business logic is fully decoupled from the HTTP layer.


Refit vs. Alternatives

Feature Refit Raw HttpClient RestSharp
Interface-based contracts
Compile-time type safety Partial Partial
DI / HttpClientFactory
Auto serialization Manual
Boilerplate required Minimal Heavy Medium
Testability Excellent Poor Moderate
Learning curve Low Medium Low

Summary

Refit is one of those libraries that, once you use it, you can't imagine going back. By expressing your API contract as a C# interface, you gain:

  • Clarity — your API surface is immediately obvious from the interface definition
  • Type safety — no more stringly-typed URLs or manual serialization
  • Testability — mock the interface, test your logic
  • Maintainability — changes to the API are localized to the interface
  • Less code — dramatically less boilerplate per endpoint

Whether you're integrating with third-party APIs or consuming your own microservices, Refit is the most expressive and productive way to write HTTP clients in .NET.


Happy coding — and may your 404s be few and your 200s be plentiful.

Top comments (0)