C# – Refit client

By | 26/11/2025

In this post, we will see what Refit is and how we can use it in our .net projects.
But first of all, what is Refit?
“Refit is a REST library for .NET that turns our REST API into a live interface. Created by Paul Betts, Refit uses C# interfaces decorated with attributes to define HTTP API endpoints, and then automatically generates the implementation at runtime using source generators or dynamic proxy generation.
Think of it as a way to define our HTTP API calls the same way we’d define any other C# interface.
Instead of manually crafting HttpClient requests, handling serialization, and parsing responses, Refit does all the heavy lifting for you.
We simply define what the API looks like, and Refit handles the rest.
Under the hood, Refit generates the necessary code to make HTTP calls, serialize request bodies, deserialize responses, and handle common HTTP scenarios.
It’s heavily inspired by Retrofit (a similar library for Android/Java), but built specifically for the .NET ecosystem.”

WHY SHOULD WE USER REFIT?

  • Type Safety
    With Refit, our API calls are strongly typed. The compiler catches errors at build time rather than runtime. If an API endpoint expects a specific model, we’ll know immediately if we’re passing the wrong type.
  • Reduced Boilerplate
    Without Refit, we’d write repetitive HttpClient code for every endpoint: creating requests, setting headers, serializing bodies, checking status codes, deserializing responses, and handling errors. Refit eliminates this boilerplate entirely.
  • Maintainability
    API definitions are centralized in interfaces. When an API changes, we update the interface, and the compiler shows we everywhere that needs updating. This is far better than hunting through strings and manual HttpClient calls scattered throughout our codebase.
  • Testability
    Because Refit uses interfaces, we can easily mock our API clients in unit tests. We can test our business logic without making actual HTTP calls.
  • Built-in Features
    Refit includes support for authentication, custom headers, multipart uploads, response streaming, and more all configured through simple attributes.

WHEN SHOULD WE USER REFIT?
We should consider using Refit in any .NET application that consumes REST APIs, especially in:

  • We’re consuming external REST APIs regularly.
  • We want compile-time checking of your API contracts.
  • We’re building a microservices architecture where services communicate via HTTP.
  • We need a clean separation between API definitions and business logic.
  • We want to reduce the amount of manual HTTP client code.


Let’s build a complete example (using a Minimal API project) that demonstrates Refit’s capabilities by creating a application that communicates with the JSONPlaceholder API, a free public REST API that’s perfect for prototyping and testing.
We’ll walk through the entire process step by step: from setting up the project and defining our API contract as a C# interface, to configuring the Refit client and making live API calls to fetch and create data.

[STEP 1: Project Setup]
First, we create a new ASP.NET Core Minimal API project and install the Refit.HttClientFactory package:

dotnet new web -n RefitMinimalApiDemo
cd RefitMinimalApiDemo
dotnet add package Refit.HttpClientFactory

[STEP 2: Define the Data Model]
The class represents the structure of a ‘Post’ returned by the API:

using System.Text.Json.Serialization;

namespace RefitMinimalApiDemo;

public class Post
{
    [JsonPropertyName("userId")]
    public int UserId { get; set; }

    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("title")]
    public string? Title { get; set; } = string.Empty;

    [JsonPropertyName("body")]
    public string? Body { get; set; } = string.Empty;
}

[STEP 3: Define the Refit Interface]
We’ll create an interface, called IJsonPlaceholderApi, that declaratively defines the API endpoints we want to use:

using Refit;

namespace RefitMinimalApiDemo;

// This interface defines the contract for our REST API.
// Refit will generate a class that implements this interface.
public interface IJsonPlaceholderApi
{
    // The [Get] attribute indicates that this method will make a GET request.
    // The string argument is the relative path that will be appended to the BaseAddress.
    // In this case, it will call "https://jsonplaceholder.typicode.com/posts".
    [Get("/posts")]
    Task<List<Post>> GetPosts();

    // Here, we define a placeholder {id} in the path.
    // The method parameter 'id' will be used to fill this placeholder.
    // Example: Calling GetPostById(1) will make a request to "/posts/1".
    [Get("/posts/{id}")]
    Task<Post> GetPostById(int id);

    // The [Post] attribute signifies a POST request.
    // The [Body] attribute tells Refit to serialize the 'post' object
    // into the request body as JSON.
    [Post("/posts")]
    Task<Post> CreatePost([Body] Post post);
}

[STEP 4: Configure and Build the Minimal API]
We’ll configure our services and define the API endpoints directly in Program.cs file:

using Refit;
using RefitMinimalApiDemo;

var builder = WebApplication.CreateBuilder(args);

// Add the API Explorer service. This is required for OpenAPI.
builder.Services.AddEndpointsApiExplorer();

// Add the Swagger generator, which depends on the API Explorer.
builder.Services.AddSwaggerGen();

// Add services to the dependency injection container.
// This is where we register our Refit client.
builder.Services
    .AddRefitClient<IJsonPlaceholderApi>()
    .ConfigureHttpClient(c =>
    {
        // Set the base address for the external API.
        c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com");
    });

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// Define an endpoint to get all posts.
// ASP.NET Core's DI automatically injects our Refit client 'api' into the handler.
app.MapGet("/posts", async (IJsonPlaceholderApi api) => await api.GetPosts())
    .WithName("GetPosts");

// Define an endpoint to get a single post by its ID.
// The 'id' from the route is passed to our handler.
app.MapGet("/posts/{id}", async (int id, IJsonPlaceholderApi api) =>
    {
        try
        {
            var post = await api.GetPostById(id);
            return Results.Ok(post);
        }
        catch (ApiException ex)
        {
            // Refit throws an ApiException for non-successful status codes.
            // This allows us to handle things like 404 Not Found gracefully.
            return Results.Problem(ex.Content, statusCode: (int)ex.StatusCode);
        }
    })
    .WithName("GetPostById");

// Define an endpoint to create a new post.
// The 'newPost' object is automatically deserialized from the request body.
app.MapPost("/posts", async (Post newPost, IJsonPlaceholderApi api) =>
    {
        var createdPost = await api.CreatePost(newPost);
        // Return a 201 Created status with the location of the new resource.
        return Results.CreatedAtRoute("GetPostById", new { id = createdPost.Id }, createdPost);
    })
    .WithName("CreatePost");

// Run the application    
app.Run();


Our API is now running and we can test it using a tool like Postman, Insomnia, or even curl command.
If we navigate to http://localhost:<port/>/swagger in our browser, we’ll see the Swagger UI where we can test the endpoints.

GetPosts:

GetPostById:

CreatePost:




Leave a Reply

Your email address will not be published. Required fields are marked *