Refit

Production-ready guide to using Refit for type-safe REST API calls with source generation in .NET

GitHub Repo stars 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 uses Roslyn source generators to create REST client implementations at compile-time, eliminating runtime reflection overhead and enabling excellent performance.

InfoLink
LicenseGitHub (MIT)
DownloadsNuget (300M+)
Latest VersionGitHub release (latest by date)
IssuesGitHub issues
ContributorsGitHub contributors (132)

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 }
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 IHttpClientFactory support
  • 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

Further Reading