Skip to main content
SEMastery
ASP.NETbeginner

Refit in .NET: Building Robust API Clients in C#

Learn Refit in .NET to build type-safe REST API clients in C#. Define an interface, add attributes, and Refit writes the HttpClient code for you.

12 min readUpdated May 2, 2026

Ordering food without learning to cook

Imagine you are hungry and you want a plate of biryani. You do not walk into the kitchen, light the stove, and cook it yourself. You open a food app, tap on the dish you want, and a little while later the food arrives at your door. You never had to know how the kitchen works. You only had to say what you wanted.

Talking to a web API from your C# code can feel like cooking in a strange kitchen. You have to build the request, set the headers, send it, wait for the reply, check if it worked, and read the JSON. That is a lot of stove-lighting just to ask for some data.

Refit is the food app for your code. You write down a short menu of what you want, and Refit takes care of all the kitchen work. You just call a normal C# method, and the data shows up. In this guide we will learn what Refit is, how it works, and how to build clean, robust API clients with it.

What is Refit?

Refit is a free, open-source library for .NET. Its job is to turn a REST API into a simple C# interface. You describe the API with an interface and a few small attributes. Refit then writes all the boring HttpClient code for you.

Here is the big idea in one picture.

You write a small interface; Refit generates the real client that talks to the API.

The name comes from a Java library called Retrofit. Refit brings the same friendly style to the .NET world. One important thing to know: modern Refit uses a Roslyn source generator. That means the real client code is created while your project builds, not while it runs. There is no slow reflection at runtime. This also lets Refit work with AOT (ahead-of-time) compilation and trimming on .NET 10 and newer.

The problem Refit solves

Let us look at the old way first. Suppose you want to fetch a user from an API. With a plain HttpClient, the code often looks like this.

public async Task<User?> GetUserAsync(int id)
{
    using var http = new HttpClient();
    http.BaseAddress = new Uri("https://api.example.com");
 
    var response = await http.GetAsync($"/users/{id}");
    response.EnsureSuccessStatusCode();
 
    var json = await response.Content.ReadAsStringAsync();
    var user = JsonSerializer.Deserialize<User>(json,
        new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
 
    return user;
}

That is a lot of code for one little request. And you have to repeat most of it for every single endpoint. It is easy to make a typo in the URL, forget to check the status code, or set up JSON wrong. Each mistake is a bug waiting to happen.

Refit removes almost all of this. You describe the call once, in one line, and you are done.

Installing Refit

Refit comes in two NuGet packages. The core package is Refit. The second package, Refit.HttpClientFactory, lets Refit plug into the built-in IHttpClientFactory so you get dependency injection, base addresses, and resilience for free.

// Run these in your project folder.
// dotnet add package Refit
// dotnet add package Refit.HttpClientFactory

For almost every real app you will want both packages. We will use both in the examples below.

Your first Refit client

Let us rebuild the user example the Refit way. First, we describe the API as an interface.

using Refit;
 
public interface IUsersApi
{
    [Get("/users/{id}")]
    Task<User> GetUserAsync(int id);
 
    [Get("/users")]
    Task<List<User>> GetAllUsersAsync();
}
 
public record User(int Id, string Name, string Email);

Look at how short that is. The [Get("/users/{id}")] attribute tells Refit two things: use the HTTP GET method, and use this relative URL. The {id} part inside the URL is matched to the id method parameter by name. Refit fills it in for you.

That is the whole client. You did not write any HttpClient code, any JSON parsing, or any URL building. Refit generates all of it during the build.

Now we register it and use it.

builder.Services
    .AddRefitClient<IUsersApi>()
    .ConfigureHttpClient(c =>
        c.BaseAddress = new Uri("https://api.example.com"));

After that, you can inject IUsersApi anywhere, just like any other service.

public class UserService(IUsersApi usersApi)
{
    public Task<User> FindAsync(int id) => usersApi.GetUserAsync(id);
}

Calling usersApi.GetUserAsync(5) sends a GET request to https://api.example.com/users/5, reads the JSON, and gives you back a User object. All of that from one line of interface code.

How a Refit call travels

It helps to see what happens, step by step, when you call a Refit method. The request flows out, and the typed result flows back.

Life of a Refit call

Call method
Build request
Send via HttpClient
Read response
Return typed object

Steps

1

Call method

You call GetUserAsync(5)

2

Build request

Refit fills the URL and headers

3

Send via HttpClient

IHttpClientFactory does the network call

4

Read response

Refit reads the JSON body

5

Return typed object

You get a User back

From a simple method call to a typed object.

And here is the same journey as a sequence, so you can see who talks to whom.

Your code calls the interface; Refit and HttpClient do the real work.

The HTTP attributes you will use most

Every method in a Refit interface needs one HTTP attribute. This attribute says which HTTP verb to use and which relative URL to hit. Refit has six built-in ones.

AttributeHTTP verbCommon use
[Get]GETRead data
[Post]POSTCreate something new
[Put]PUTReplace an item fully
[Patch]PATCHUpdate part of an item
[Delete]DELETERemove an item
[Head]HEADCheck headers only

You pick the attribute that matches what the API expects, give it the relative URL, and Refit does the rest.

Passing data: route, query, and body

Most real requests carry some data. Refit gives you clean ways to send it.

Route values are filled from method parameters with matching names. We already saw this with {id}.

Query string values are added automatically for any parameter that does not match a route placeholder. So this method:

[Get("/users")]
Task<List<User>> SearchAsync(string name, int page);

becomes a request like GET /users?name=ravi&page=2. Refit builds the query string for you.

Request bodies use the [Body] attribute. When you create or update something, you usually send an object as JSON in the body.

public interface IUsersApi
{
    [Post("/users")]
    Task<User> CreateUserAsync([Body] CreateUserRequest request);
}
 
public record CreateUserRequest(string Name, string Email);

Refit serializes the CreateUserRequest to JSON and puts it in the POST body. The response JSON comes back as a User. You never touch the serializer.

Sometimes the C# name you want is not the same as the API's name. The [AliasAs] attribute fixes that. For example, if the route says id but you want a clearer parameter name:

[Get("/group/{id}/users")]
Task<List<User>> GroupUsersAsync([AliasAs("id")] int groupId);

Here is a quick map of these tools.

Refit featureWhat it doesExample
Route placeholderFills part of the URL[Get("/users/{id}")]
Query parameterAdds to the query stringSearchAsync(string name)
[Body]Sends an object as the request bodyCreate([Body] req)
[AliasAs]Renames a parameter for the API[AliasAs("id")] int groupId
[Header]Sends a value as an HTTP header[Header("X-Trace")] string trace

Adding headers

APIs often need headers, like an authorization token or an API key. Refit lets you add headers in a few ways.

For a header that is always the same, put [Headers] on the method or the whole interface.

[Headers("Accept: application/json")]
public interface IUsersApi
{
    [Get("/users/{id}")]
    [Headers("X-Source: web-app")]
    Task<User> GetUserAsync(int id);
}

For a header whose value changes per call, such as a bearer token, use the [Header] attribute on a parameter.

[Get("/me")]
Task<User> GetMeAsync([Header("Authorization")] string bearerToken);

For tokens that should be attached to every call, a cleaner approach is a DelegatingHandler registered with the HTTP client. That keeps the token logic out of your interface entirely. We have a separate guide on delegating handlers if you want to go deeper.

Handling errors the safe way

Things go wrong on the network. A user might not exist. A server might be down. Refit gives you two clear styles for dealing with this.

Style one: exceptions. By default, when the server returns a failure status code (like 404 or 500), Refit throws an ApiException. This exception is rich. It holds the status code, the request, the response headers, and the raw error body.

try
{
    var user = await usersApi.GetUserAsync(999);
}
catch (ApiException ex)
{
    if (ex.StatusCode == HttpStatusCode.NotFound)
    {
        // No such user. Handle it gently.
    }
 
    var rawError = ex.Content; // the error body as text
}

Style two: ApiResponse. If you do not like exceptions for normal "not found" cases, change the return type to ApiResponse<T>. Now Refit will not throw. Instead it hands you an object that carries the status, the headers, the content, and any error as data.

[Get("/users/{id}")]
Task<ApiResponse<User>> TryGetUserAsync(int id);
 
// usage
var response = await usersApi.TryGetUserAsync(999);
if (response.IsSuccessStatusCode)
{
    User user = response.Content!;
}
else
{
    var error = response.Error; // ApiException, but not thrown
}

This decision tree shows how to pick a style.

Choose exceptions for unexpected failures and ApiResponse when you want failure as data.

Making clients robust with resilience

A robust client does more than send a request. It also survives a slow network. Because Refit plugs into IHttpClientFactory, you can add retries, timeouts, and circuit breakers without changing your interface at all.

The modern way is the Microsoft.Extensions.Http.Resilience package, which is built on Polly.

builder.Services
    .AddRefitClient<IUsersApi>()
    .ConfigureHttpClient(c =>
        c.BaseAddress = new Uri("https://api.example.com"))
    .AddStandardResilienceHandler();

That single AddStandardResilienceHandler() call adds sensible retries, a timeout, and a circuit breaker. If the API hiccups, your client quietly retries before giving up. Your interface code stays clean because all of this lives in the registration.

A robust Refit pipeline

Interface
HttpClientFactory
Resilience
Auth handler
Network

Steps

1

Interface

Your typed methods

2

HttpClientFactory

Base address and lifetime

3

Resilience

Retries and timeouts

4

Auth handler

Adds the token

5

Network

The real request goes out

Layers added around the same simple interface.

Testing code that uses Refit

Because a Refit client is just an interface, testing the code around it is easy. You do not need a real server. You can create a fake IUsersApi with any mocking library and check that your service behaves correctly.

[Fact]
public async Task FindAsync_returns_user()
{
    var fakeApi = Substitute.For<IUsersApi>();
    fakeApi.GetUserAsync(5)
        .Returns(new User(5, "Ravi", "[email protected]"));
 
    var service = new UserService(fakeApi);
 
    var user = await service.FindAsync(5);
 
    Assert.Equal("Ravi", user.Name);
}

This is one of the quiet wins of Refit. Your business code depends on a small interface, not on HttpClient, so it is simple to test in isolation.

When should you use Refit?

Refit shines when you call REST APIs that return JSON and you want clean, type-safe code. It is great for talking to your own microservices, third-party APIs, and gateways.

It is less suited to very unusual APIs, like streaming endpoints with custom framing, or protocols that are not really REST. For those, a hand-written HttpClient or a purpose-built SDK may fit better. For the everyday job of "call this API and give me an object back," Refit is hard to beat.

A small note on the wider .NET ecosystem: some popular libraries such as MediatR and MassTransit have moved to commercial licensing. Refit is not in that group. It remains free and open source under the MIT license, so you can use it in personal and commercial projects without a fee.

Quick recap

  • Refit turns a REST API into a C# interface. You describe the calls; Refit writes the HttpClient code.
  • It uses a source generator, so the client is built at compile time. There is no slow runtime reflection, and it works with AOT and trimming on modern .NET.
  • Install two packages: Refit for the core, and Refit.HttpClientFactory to plug into dependency injection.
  • Use HTTP attributes like [Get], [Post], [Put], [Patch], and [Delete] on interface methods.
  • Pass data through route placeholders, query parameters, and the [Body] attribute. Use [AliasAs] and [Header] for the tricky cases.
  • Handle errors with ApiException (it throws) or ApiResponse<T> (failure as data).
  • Add resilience with AddStandardResilienceHandler() for retries and timeouts, all without touching your interface.
  • Testing is easy because your code depends on a small interface you can fake.

References and further reading

Related Posts