| name | refit |
| description | USE FOR: Defining type-safe HTTP API clients using interfaces and attributes with Refit's source generator. Use when consuming REST APIs where compile-time safety, automatic serialization, and integration with HttpClientFactory and DI are needed.
DO NOT USE FOR: Low-level HTTP operations requiring manual stream control (use HttpClient directly), GraphQL clients (use StrawberryShake or GraphQL.Client), or file download/upload with custom progress reporting (use HttpClient with stream copying).
|
| license | MIT |
| metadata | {"displayName":"Refit","author":"Tyler-R-Kendrick","version":"1.0.0"} |
| compatibility | ["claude","copilot","cursor"] |
| references | [{"title":"Refit GitHub Repository","url":"https://github.com/reactiveui/refit"},{"title":"Refit NuGet Package","url":"https://www.nuget.org/packages/Refit"}] |
Refit
Overview
Refit is a type-safe REST client library for .NET that turns interface definitions into live HTTP clients using source generators. You define an interface with attributes like [Get], [Post], [Put], and [Delete] on methods, and Refit generates the implementation at compile time. Refit handles URL interpolation, query string building, JSON serialization/deserialization, multipart uploads, and header management. It integrates with HttpClientFactory for proper handler lifecycle management and with ASP.NET Core's dependency injection for clean service registration. Refit uses System.Text.Json by default and supports Newtonsoft.Json as an alternative serializer.
Defining API Interfaces
Create interfaces with HTTP method attributes that map to API endpoints.
using Refit;
namespace MyApp.ApiClients;
public interface IProductsApi
{
[Get("/api/products")]
Task<List<ProductDto>> GetAllAsync(
[Query] int page = 1,
[Query] int pageSize = 20,
[Query] string? category = null);
[Get("/api/products/{id}")]
Task<ProductDto> GetByIdAsync(int id);
[Post("/api/products")]
Task<ProductDto> CreateAsync([Body] CreateProductRequest request);
[Put("/api/products/{id}")]
Task<ProductDto> UpdateAsync(int id, [Body] UpdateProductRequest request);
[Delete("/api/products/{id}")]
Task DeleteAsync(int id);
[Get("/api/products/search")]
Task<List<ProductDto>> SearchAsync([Query] ProductSearchQuery query);
[Multipart]
[Post("/api/products/{id}/image")]
Task UploadImageAsync(int id, [AliasAs("file")] StreamPart image);
}
();
;
;
;
ASP.NET Core Registration with HttpClientFactory
Register Refit clients with the DI container using AddRefitClient.
using Refit;
using MyApp.ApiClients;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddRefitClient<IProductsApi>()
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri("https://api.products.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddStandardResilienceHandler();
builder.Services
.AddRefitClient<IOrdersApi>(new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(
new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
})
})
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri("https://api.orders.com");
})
.AddHttpMessageHandler<AuthHeaderHandler>();
var app = builder.Build();
app.MapGet("/products", async (IProductsApi api, int page = 1) =>
Results.Ok(await api.GetAllAsync(page)));
app.Run();
Authentication with Delegating Handlers
Add authentication tokens to requests using DelegatingHandler.
using System.Net.Http.Headers;
namespace MyApp.Http;
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.GetAccessTokenAsync(cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
builder.Services.AddTransient<AuthHeaderHandler>();
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services
.AddRefitClient<IProductsApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.products.com"))
.AddHttpMessageHandler<AuthHeaderHandler>();
Error Handling with ApiException
Handle API errors using Refit's ApiException and ApiResponse<T>.
using Refit;
namespace MyApp.ApiClients;
public interface IOrdersApi
{
[Get("/api/orders/{id}")]
Task<ApiResponse<OrderDto>> GetByIdAsync(int id);
[Post("/api/orders")]
Task<ApiResponse<OrderDto>> CreateAsync([Body] CreateOrderRequest request);
}
public class OrderService
{
private readonly IOrdersApi _api;
private readonly ILogger<OrderService> _logger;
public OrderService(IOrdersApi api, ILogger<OrderService> logger)
{
_api = api;
_logger = logger;
}
public async Task<OrderResult> GetOrderAsync(int id)
{
using var response = await _api.GetByIdAsync(id);
if (response.IsSuccessStatusCode && response.Content is not null)
{
return OrderResult.Success(response.Content);
}
_logger.LogWarning(
"Failed to get order {Id}. Status: {Status}, Error: {Error}",
id,
response.StatusCode,
response.Error?.Content);
return response.StatusCode switch
{
System.Net.HttpStatusCode.NotFound => OrderResult.NotFound(),
System.Net.HttpStatusCode.Unauthorized => OrderResult.Unauthorized(),
_ => OrderResult.Error()
};
}
}
{
IProductsApi _api;
=> _api = api;
Task<ProductDto?> GetProductAsync( id)
{
{
_api.GetByIdAsync(id);
}
(ApiException ex) (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
;
}
(ApiException ex)
{
errorBody = ex.GetContentAsAsync<ProblemDetails>();
ExternalApiException(
,
ex);
}
}
}
Headers and Dynamic Configuration
Use static and dynamic headers on interfaces and methods.
using Refit;
namespace MyApp.ApiClients;
[Headers("User-Agent: MyApp/1.0", "Accept: application/json")]
public interface IExternalApi
{
[Get("/data")]
[Headers("Cache-Control: no-cache")]
Task<DataResponse> GetFreshDataAsync();
[Get("/data")]
Task<DataResponse> GetCachedDataAsync();
[Post("/data")]
Task<DataResponse> CreateDataAsync(
[Body] DataRequest request,
[Header("X-Idempotency-Key")] string idempotencyKey,
[Header("X-Request-Id")] string requestId);
[Get("/data")]
Task<DataResponse> GetDataWithAuthAsync(
[Authorize("Bearer")] string token);
}
Refit vs Other HTTP Client Libraries
| Feature | Refit | RestSharp | HttpClient | Flurl |
|---|
| Approach | Interface + attributes | Builder pattern | Manual requests | Fluent URL builder |
| Code generation | Source generator | None | None | None |
| Type safety | Compile-time | Runtime | Runtime | Runtime |
| Serialization | Auto (STJ / Newtonsoft) | Auto (STJ / Newtonsoft) | Manual | Auto (STJ / Newtonsoft) |
| HttpClientFactory | Built-in integration | v107+ support | Native | Built-in integration |
| Error handling | ApiException / ApiResponse | RestResponse | HttpResponseMessage | FlurlHttpException |
| Multipart upload | [Multipart] + StreamPart | AddFile() | MultipartFormDataContent | PostMultipartAsync() |
| Query objects | [Query] on complex types | AddQueryParameter() | Manual string building | SetQueryParams() |
Best Practices
-
Define one Refit interface per API domain (e.g., IProductsApi, IOrdersApi, IUsersApi) rather than one monolithic interface, so that each interface can be registered with different HttpClient configurations (base URL, timeout, retry policy) and injected only where needed.
-
Use AddRefitClient<T>() with ConfigureHttpClient() instead of RestService.For<T>() so that Refit clients participate in HttpClientFactory's handler lifecycle management, preventing socket exhaustion from creating HttpClient instances manually.
-
Return Task<ApiResponse<T>> instead of Task<T> on interface methods when the calling code needs to inspect HTTP status codes, headers, or error bodies without catching exceptions, because ApiResponse<T> wraps the response metadata and deserialized content together.
-
Add resilience with .AddStandardResilienceHandler() from Microsoft.Extensions.Http.Resilience on AddRefitClient registrations to automatically retry transient failures, apply circuit breakers, and enforce timeouts, rather than implementing retry logic in the consuming service.
-
Use [Query] on complex type parameters to automatically flatten object properties into query string parameters (?term=shoes&minPrice=10&maxPrice=100) instead of building query strings manually, keeping method signatures clean and type-safe.
-
Implement DelegatingHandler for cross-cutting concerns (authentication, logging, correlation IDs) and register them with .AddHttpMessageHandler<T>(), so that all requests through a Refit client include the required headers without modifying each interface method.
-
Use [Authorize("Bearer")] on a string parameter for per-request token injection in scenarios where different calls may use different tokens (multi-tenant, user impersonation), instead of a shared DelegatingHandler that always attaches the same token.
-
Configure RefitSettings with SystemTextJsonContentSerializer and explicit (camelCase naming, null handling) to ensure serialization matches the API contract, rather than relying on default serializer settings that may differ between .NET versions.