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
If you're using ASP.NET Core and want seamless HttpClientFactory integration:
dotnet add package Refit.HttpClientFactory
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);
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);
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);
}
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);
}
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);
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);
}
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);
Need to send form data instead of JSON?
[Post("/oauth/token")]
Task<TokenResponse> GetTokenAsync([Body(BodySerializationMethod.UrlEncoded)] TokenRequest request);
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}");
}
You can also use ApiResponse<T> (concrete class) to access response headers:
var response = await api.GetProductAsync(42);
var etag = response.Headers.ETag?.Tag;
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"));
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);
}
}
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);
}
}
Register it with DI:
builder.Services.AddTransient<AuthHeaderHandler>();
builder.Services
.AddRefitClient<ISecureApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler<AuthHeaderHandler>();
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);
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
})
};
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");
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);
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);
}
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}");
}
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);
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)