Refit
Production-ready guide to using Refit for type-safe REST API calls with source generation in .NET
6 minute read
is the most powerful and performant type-safe REST client library in the .NET ecosystem, with 9.4k+ GitHub stars and used by 11,700+ projects.
Refit 9.x - Latest Version
**Refit 9.0.2** (November 2025) includes AOT and trimming support for .NET 10+, making it ideal for modern cloud-native applications.Refit uses Roslyn source generators to create REST client implementations at compile-time, eliminating runtime reflection overhead and enabling excellent performance.
| Info | Link |
|---|---|
| License | |
| Downloads | |
| Latest Version | |
| Issues | |
| Contributors |
How Refit Works
Refit transforms interface definitions into fully-implemented HTTP clients at compile time:
graph LR
A[Interface Definition] --> B[Roslyn Source Generator]
B --> C[Generated Implementation]
C --> D[HttpClient Calls]This approach provides:
- Zero runtime reflection - All code generated at compile time
- Type safety - Compile-time verification of API contracts
- IntelliSense support - Full IDE support for API methods
- AOT compatibility - Works with Native AOT in .NET 10+
Quick Start
1. Install NuGet Packages
# Core Refit package
dotnet add package Refit
# For ASP.NET Core / IHttpClientFactory integration
dotnet add package Refit.HttpClientFactory
# Optional: Newtonsoft.Json serializer (System.Text.Json is default)
dotnet add package Refit.Newtonsoft.Json
2. Define Your API Interface
Refit uses interface definitions with HTTP attributes to describe REST APIs:
using Refit;
/// <summary>
/// Type-safe client for the Tasks REST API.
/// Implementation is generated by Refit at compile time.
/// </summary>
public interface ITasksApi
{
/// <summary>Retrieves all tasks, optionally filtered by status.</summary>
[Get("/api/v1/tasks")]
Task<IReadOnlyList<TaskDto>> GetAllTasksAsync(
[Query] string? status = null,
CancellationToken cancellationToken = default);
/// <summary>Retrieves a specific task by its identifier.</summary>
[Get("/api/v1/tasks/{taskId}")]
Task<TaskDto> GetTaskByIdAsync(
int taskId,
CancellationToken cancellationToken = default);
/// <summary>Creates a new task.</summary>
[Post("/api/v1/tasks")]
Task<TaskDto> CreateTaskAsync(
[Body] CreateTaskRequest request,
CancellationToken cancellationToken = default);
/// <summary>Updates an existing task.</summary>
[Put("/api/v1/tasks/{taskId}")]
Task<TaskDto> UpdateTaskAsync(
int taskId,
[Body] UpdateTaskRequest request,
CancellationToken cancellationToken = default);
/// <summary>Partially updates a task.</summary>
[Patch("/api/v1/tasks/{taskId}")]
Task<TaskDto> PatchTaskAsync(
int taskId,
[Body] PatchTaskRequest request,
CancellationToken cancellationToken = default);
/// <summary>Deletes a task.</summary>
[Delete("/api/v1/tasks/{taskId}")]
Task DeleteTaskAsync(
int taskId,
CancellationToken cancellationToken = default);
/// <summary>Marks a task as completed.</summary>
[Put("/api/v1/tasks/{taskId}/complete")]
Task<TaskDto> CompleteTaskAsync(
int taskId,
CancellationToken cancellationToken = default);
}
// DTOs with explicit types
public sealed record TaskDto(
int Id,
string Title,
string? Description,
bool IsCompleted,
DateTime CreatedAt,
TaskPriority Priority);
public sealed record CreateTaskRequest(
string Title,
string? Description = null,
TaskPriority Priority = TaskPriority.Medium);
public sealed record UpdateTaskRequest(
string Title,
string? Description,
TaskPriority Priority);
public sealed record PatchTaskRequest(
string? Title = null,
string? Description = null,
bool? IsCompleted = null);
public enum TaskPriority { Low, Medium, High, Critical }
3. Register with Dependency Injection (Recommended)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Resilience;
using Refit;
IServiceCollection services = new ServiceCollection();
// Register Refit client with IHttpClientFactory
services
.AddRefitClient<ITasksApi>()
.ConfigureHttpClient((IServiceProvider provider, HttpClient client) =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(30);
})
// Add resilience (retry, circuit breaker, timeout)
.AddStandardResilienceHandler();
// Optional: Configure custom RefitSettings
services.AddRefitClient<ITasksApi>(provider => new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
})
});
4. Use the API Client
using Microsoft.Extensions.DependencyInjection;
// Resolve from DI
IServiceProvider serviceProvider = services.BuildServiceProvider();
ITasksApi tasksApi = serviceProvider.GetRequiredService<ITasksApi>();
// GET - Retrieve tasks
IReadOnlyList<TaskDto> allTasks = await tasksApi.GetAllTasksAsync();
IReadOnlyList<TaskDto> pendingTasks = await tasksApi.GetAllTasksAsync(status: "pending");
TaskDto task = await tasksApi.GetTaskByIdAsync(123);
// POST - Create task
CreateTaskRequest createRequest = new(
Title: "Review documentation",
Description: "Review the updated API docs",
Priority: TaskPriority.High);
TaskDto createdTask = await tasksApi.CreateTaskAsync(createRequest);
Console.WriteLine($"Created task #{createdTask.Id}");
// PUT - Update task
UpdateTaskRequest updateRequest = new(
Title: "Review documentation (Updated)",
Description: "Review and approve the updated API docs",
Priority: TaskPriority.Critical);
TaskDto updatedTask = await tasksApi.UpdateTaskAsync(createdTask.Id, updateRequest);
// PATCH - Partial update
PatchTaskRequest patchRequest = new(IsCompleted: true);
TaskDto patchedTask = await tasksApi.PatchTaskAsync(createdTask.Id, patchRequest);
// DELETE - Remove task
await tasksApi.DeleteTaskAsync(createdTask.Id);
Alternative: Direct Instantiation (Without DI)
using Refit;
// For simple scenarios without dependency injection
ITasksApi tasksApi = RestService.For<ITasksApi>("https://api.example.com");
TaskDto task = await tasksApi.GetTaskByIdAsync(123);
Advanced Features
Query Parameters
public interface ISearchApi
{
// Simple query parameters
[Get("/search")]
Task<SearchResult> SearchAsync(
[Query] string query,
[Query] int page = 1,
[Query] int pageSize = 20,
CancellationToken cancellationToken = default);
// Query parameter with alias
[Get("/search")]
Task<SearchResult> SearchWithAliasAsync(
[Query] string q,
[AliasAs("max_results")] [Query] int maxResults = 50,
CancellationToken cancellationToken = default);
// Complex object as query parameters
[Get("/search")]
Task<SearchResult> SearchWithObjectAsync(
[Query] SearchFilters filters,
CancellationToken cancellationToken = default);
// Collection as query parameter
[Get("/items")]
Task<List<Item>> GetItemsAsync(
[Query(CollectionFormat.Multi)] int[] ids,
CancellationToken cancellationToken = default);
// Results in: /items?ids=1&ids=2&ids=3
}
public sealed record SearchFilters(
string? Query,
string? Category,
DateTime? FromDate,
DateTime? ToDate);
Headers
// Static headers on interface
[Headers("User-Agent: MyApp/1.0", "Accept: application/json")]
public interface IApiWithHeaders
{
// Static headers on method
[Headers("Cache-Control: no-cache")]
[Get("/data")]
Task<Data> GetDataAsync(CancellationToken cancellationToken = default);
// Dynamic headers via parameter
[Get("/secure-data")]
Task<SecureData> GetSecureDataAsync(
[Header("Authorization")] string authorization,
[Header("X-Request-Id")] string requestId,
CancellationToken cancellationToken = default);
// Authorize attribute for Bearer tokens (shorthand)
[Get("/protected")]
Task<ProtectedResource> GetProtectedAsync(
[Authorize("Bearer")] string token,
CancellationToken cancellationToken = default);
// Header collection
[Get("/flexible")]
Task<Response> GetWithHeadersAsync(
[HeaderCollection] IDictionary<string, string> headers,
CancellationToken cancellationToken = default);
}
Error Handling with ApiResponse
using Refit;
public interface ITasksApiWithResponse
{
// Returns ApiResponse for detailed response information
[Get("/api/v1/tasks/{taskId}")]
Task<ApiResponse<TaskDto>> GetTaskWithResponseAsync(
int taskId,
CancellationToken cancellationToken = default);
// IApiResponse interface for flexibility
[Get("/api/v1/tasks")]
Task<IApiResponse<IReadOnlyList<TaskDto>>> GetTasksWithResponseAsync(
CancellationToken cancellationToken = default);
}
// Usage with detailed error handling
public async Task HandleApiResponseAsync(ITasksApiWithResponse api)
{
ApiResponse<TaskDto> response = await api.GetTaskWithResponseAsync(123);
if (response.IsSuccessStatusCode)
{
TaskDto task = response.Content!;
Console.WriteLine($"Task: {task.Title}");
// Access response headers
IEnumerable<string>? requestId = response.Headers.GetValues("X-Request-Id");
}
else
{
// Handle error
HttpStatusCode statusCode = response.StatusCode;
string? errorContent = response.Error?.Content;
Console.WriteLine($"Error {(int)statusCode}: {errorContent}");
}
}
// Alternative: Catching ApiException
public async Task HandleExceptionAsync(ITasksApi api)
{
try
{
TaskDto task = await api.GetTaskByIdAsync(999);
}
catch (ApiException ex)
{
Console.WriteLine($"API Error: {ex.StatusCode}");
Console.WriteLine($"Content: {ex.Content}");
// Deserialize error response if structured
ProblemDetails? problem = await ex.GetContentAsAsync<ProblemDetails>();
}
catch (ValidationApiException ex)
{
// RFC 7807 Problem Details support
Console.WriteLine($"Validation Error: {ex.Content?.Title}");
foreach (KeyValuePair<string, object> error in ex.Content?.Extensions ?? new())
{
Console.WriteLine($" {error.Key}: {error.Value}");
}
}
}
Bearer Authentication with DelegatingHandler
using System.Net.Http.Headers;
/// <summary>
/// Handler that automatically adds Bearer tokens to all requests.
/// </summary>
public sealed class AuthorizationHandler : DelegatingHandler
{
private readonly ITokenService _tokenService;
public AuthorizationHandler(ITokenService tokenService)
{
_tokenService = tokenService;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
string token = await _tokenService.GetTokenAsync(cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
// Registration
services.AddTransient<ITokenService, TokenService>();
services.AddTransient<AuthorizationHandler>();
services
.AddRefitClient<ITasksApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler<AuthorizationHandler>();
Multipart File Upload
using Refit;
public interface IFileUploadApi
{
[Multipart]
[Post("/files/upload")]
Task<UploadResult> UploadFileAsync(
[AliasAs("file")] StreamPart file,
[AliasAs("description")] string description,
CancellationToken cancellationToken = default);
[Multipart]
[Post("/files/upload-multiple")]
Task<UploadResult> UploadMultipleFilesAsync(
[AliasAs("files")] IEnumerable<StreamPart> files,
CancellationToken cancellationToken = default);
}
// Usage
public async Task UploadAsync(IFileUploadApi api)
{
await using FileStream fileStream = File.OpenRead("document.pdf");
StreamPart streamPart = new(fileStream, "document.pdf", "application/pdf");
UploadResult result = await api.UploadFileAsync(streamPart, "My document");
}
✅ Pros
- Type-safe - Compile-time API contract verification
- High performance - Source generation eliminates reflection
- Minimal boilerplate - Just define interfaces
- AOT compatible - Works with .NET Native AOT
- Excellent DI integration - First-class
IHttpClientFactorysupport - Rich features - Headers, auth, multipart, streaming
- Active development - Regular updates and improvements
⚠️ Considerations
- Source generators - May complicate debugging
- Interface required - One interface per API
- Build-time dependency - Roslyn analyzer/generator
- Learning curve - Attribute-based syntax
Full Sample
See the full sample on GitHub: https://github.com/BenjaminAbt/dotnet.rest-samples