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.
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.
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.HttpClientFactoryFor 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
Steps
Call method
You call GetUserAsync(5)
Build request
Refit fills the URL and headers
Send via HttpClient
IHttpClientFactory does the network call
Read response
Refit reads the JSON body
Return typed object
You get a User back
And here is the same journey as a sequence, so you can see who talks to whom.
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.
| Attribute | HTTP verb | Common use |
|---|---|---|
[Get] | GET | Read data |
[Post] | POST | Create something new |
[Put] | PUT | Replace an item fully |
[Patch] | PATCH | Update part of an item |
[Delete] | DELETE | Remove an item |
[Head] | HEAD | Check 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 feature | What it does | Example |
|---|---|---|
| Route placeholder | Fills part of the URL | [Get("/users/{id}")] |
| Query parameter | Adds to the query string | SearchAsync(string name) |
[Body] | Sends an object as the request body | Create([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.
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
Steps
Interface
Your typed methods
HttpClientFactory
Base address and lifetime
Resilience
Retries and timeouts
Auth handler
Adds the token
Network
The real request goes out
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
HttpClientcode. - 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:
Refitfor the core, andRefit.HttpClientFactoryto 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) orApiResponse<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
- Refit on GitHub — official documentation
- Refit on NuGet
- HTTP requests with IHttpClientFactory — Microsoft Learn
- Build resilient HTTP apps — Microsoft Learn
- Using Refit to Consume APIs in C# — Code Maze
Related Posts
Extending HttpClient With Delegating Handlers in ASP.NET Core
Learn how DelegatingHandlers build a middleware pipeline for HttpClient in ASP.NET Core to add logging, auth, and retries with IHttpClientFactory.
Building Async APIs in ASP.NET Core the Right Way
Learn to build fast, safe async APIs in ASP.NET Core: async/await, CancellationToken, avoiding .Result deadlocks, and thread pool tips.
Best Practices for Building REST APIs in ASP.NET Core
A friendly, beginner guide to REST API best practices in ASP.NET Core with naming, status codes, validation, ProblemDetails, paging, versioning, security, and code.
Logging Requests and Responses for APIs and HttpClient in ASP.NET Core
Learn to log incoming API requests and outgoing HttpClient calls in ASP.NET Core using built-in HTTP logging and a custom DelegatingHandler, step by step.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.
The 5 Most Common REST API Design Mistakes (and How to Avoid Them)
A warm beginner guide to the 5 most common REST API design mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# code examples.