Refit: Typed HTTP Clients for .NET
Calling external APIs from .NET usually means writing repetitive HttpClient code: constructing URLs, serialising request bodies, deserialising responses, handling errors. Refit eliminates this boilerplate by generating HTTP client implementations from interface definitions at compile time. You define the API contract as a C# interface, and Refit handles the rest.
The Problem
A typical HttpClient call looks like this:
var response = await httpClient.GetAsync($"/api/orders/{orderId}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var order = JsonSerializer.Deserialize<Order>(json, options);
Multiply this across dozens of endpoints and multiple services, and you have a significant amount of repetitive, error-prone code. URL typos, serialisation mismatches, and inconsistent error handling creep in.
Defining an API with Refit
Install the Refit.HttpClientFactory package and define your API as an interface:
public interface IOrderService
{
[Get("/api/orders")]
Task<List<Order>> GetOrdersAsync(CancellationToken ct = default);
[Get("/api/orders/{id}")]
Task<Order> GetOrderAsync(int id, CancellationToken ct = default);
[Post("/api/orders")]
Task<Order> CreateOrderAsync([Body] CreateOrderRequest request, CancellationToken ct = default);
[Put("/api/orders/{id}")]
Task UpdateOrderAsync(int id, [Body] UpdateOrderRequest request, CancellationToken ct = default);
[Delete("/api/orders/{id}")]
Task DeleteOrderAsync(int id, CancellationToken ct = default);
}
Refit uses source generators to create the implementation at compile time. No reflection, no runtime code generation.
Registration and Dependency Injection
Register the Refit client with HttpClientFactory:
builder.Services
.AddRefitClient<IOrderService>()
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://orders.example.com");
c.DefaultRequestHeaders.Add("Accept", "application/json");
});
Inject IOrderService wherever you need it:
public class OrderController(IOrderService orderService)
{
public async Task<IResult> GetOrder(int id, CancellationToken ct)
{
var order = await orderService.GetOrderAsync(id, ct);
return Results.Ok(order);
}
}
The HttpClientFactory integration means you get proper HttpClient lifecycle management, connection pooling, and the ability to add delegating handlers.
Query Parameters
Pass query parameters as method arguments. Refit maps them automatically:
[Get("/api/orders")]
Task<List<Order>> SearchOrdersAsync(
[Query] string? status = null,
[Query] int page = 1,
[Query] int pageSize = 20,
CancellationToken ct = default);
This generates a request like /api/orders?status=confirmed&page=1&pageSize=20. For complex query objects, use [Query] on a class parameter:
public class OrderSearchQuery
{
public string? Status { get; set; }
public DateTime? FromDate { get; set; }
public int Page { get; set; } = 1;
}
[Get("/api/orders")]
Task<List<Order>> SearchOrdersAsync([Query] OrderSearchQuery query, CancellationToken ct = default);
Headers
Add static headers with attributes or dynamic headers with parameters:
[Headers("X-Api-Key: my-key")]
public interface IOrderService
{
[Get("/api/orders/{id}")]
Task<Order> GetOrderAsync(int id, CancellationToken ct = default);
[Post("/api/orders")]
Task<Order> CreateOrderAsync(
[Body] CreateOrderRequest request,
[Header("Idempotency-Key")] string idempotencyKey,
CancellationToken ct = default);
}
For authentication tokens that change per request, use a delegating handler:
public class AuthHeaderHandler(ITokenProvider tokenProvider) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
var token = await tokenProvider.GetTokenAsync(ct);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, ct);
}
}
Register it in the pipeline:
builder.Services.AddTransient<AuthHeaderHandler>();
builder.Services
.AddRefitClient<IOrderService>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://orders.example.com"))
.AddHttpMessageHandler<AuthHeaderHandler>();
Error Handling
By default, Refit throws an ApiException for non-success status codes. You can access the response details:
try
{
var order = await orderService.GetOrderAsync(42);
}
catch (ApiException ex)
{
var problemDetails = await ex.GetContentAsAsync<ProblemDetails>();
logger.LogWarning("API error: {Status} - {Detail}",
ex.StatusCode, problemDetails?.Detail);
}
Alternatively, use IApiResponse<T> to avoid exceptions for expected error cases:
[Get("/api/orders/{id}")]
Task<IApiResponse<Order>> GetOrderAsync(int id, CancellationToken ct = default);
// Usage
var response = await orderService.GetOrderAsync(42);
if (response.IsSuccessStatusCode)
{
var order = response.Content;
}
else
{
logger.LogWarning("Failed: {StatusCode}", response.StatusCode);
}
This pattern is cleaner when 404s or other non-success responses are normal flow rather than exceptional cases.
Resilience with Polly
Combine Refit with Microsoft.Extensions.Http.Resilience for retry and circuit-breaker policies:
builder.Services
.AddRefitClient<IOrderService>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://orders.example.com"))
.AddStandardResilienceHandler();
The standard resilience handler adds retry, circuit breaker, and timeout policies with sensible defaults. No additional configuration needed for most services.
When to Use Refit
Refit is ideal when you consume well-defined HTTP APIs with stable contracts. It pairs naturally with OpenAPI specifications — you can generate the interface from a spec, or write it by hand for smaller APIs. For one-off HTTP calls or highly dynamic endpoints, raw HttpClient may be simpler. But for any service you call regularly, Refit removes boilerplate and makes the API contract explicit and testable.