Updated on

Refit describes itself as “the automatic type-safe REST library for modern .NET“, and what that means in practice is that it turns an API into a C# interface. We declare the endpoints as methods with attributes ([Get("/users/{id}")]), and Refit generates the code that builds the request, sends it, and deserializes the response.

The result is that calling a remote API looks like calling a local service. There is no HttpRequestMessage to assemble, no JsonSerializer call, and no string concatenation in a URL, so the mistakes those three invite disappear along with them.

To download the source code for the video, visit our Patreon page (YouTube Patron tier).

VIDEO: Refit - Great API Client To Turn Your REST API Into a Live Interface.


What Is Refit in C#?

Refit is an open-source library that generates a typed HTTP client from an interface we declare.

We write the interface (one method per endpoint, each decorated with the verb and the route), and Refit writes the implementation. [Get("/users/{id}")] Task<User> GetUser(int id); becomes a real HTTP GET, with the argument substituted into the path and the JSON response deserialized into a User.

Refit’s README is explicit: “The Refit package ships Roslyn source generators.” Generation happens at build time, so the code that actually runs is there in the project for us to read.

What we get back is an ordinary interface, which matters more than it sounds. It registers in the dependency injection container like any other service, and it mocks in a unit test like any other service: no HttpMessageHandler stub, no hand-built fake response pipeline.

If you want to learn more about HttpClient, check out our series on HttpClient with ASP.NET Core.

With this interface, we define the endpoints (GET, POST, PUT) our API contains, along with any route or body parameters. Also, we can include headers in the interface, such as ones for Authorization.

Components of a Refit Client

Before creating an application to demonstrate Refit, let’s explore some of the main components that make up a Refit client.

HTTP Methods

Any time we interact with an API over HTTP, we must be familiar with the different HTTP methods available to us, and how they work. Refit provides a set of attributes that allow us to decorate our interface methods:

[Get("/users")]
Task<IEnumerable<User>> GetUsers();

By decorating the GetUsers() method with [Get("/users")], we tell Refit this is an HTTP GET method, to the /users endpoint.

Refit provides attributes for all the common HTTP methods.

Route Parameters

When working with RESTful APIs that follow good routing conventions, we’ll often see an endpoint like /users/1, which we would expect to return us a user with id 1. Refit uses attribute routing, the same as ASP.NET Core, that allows us to easily define routes that contain parameters:

[Get("/users/{id}")]
Task<User> GetUser(int id);

By adding { and } around id in the route, we tell Refit that this is a dynamic parameter that comes from the id parameter in the GetUser() method.

Request and Response Serialization

The most common way to send data over HTTP is by serializing it as JSON and adding it to the request body. Refit provides this automatically for us.

This allows us to provide classes as parameters to a Refit method, and also specify them as the return type that we expect to be returned from the API:

[Put("/users/{id}")]
Task<User> UpdateUser(int id, User user);

Refit will automatically serialize the user parameter to JSON when sending the request and will attempt to deserialize the response into a User object.

Instantiating a Refit Client

Refit provides us with two ways to instantiate a client, either by using the RestService class provided by Refit, or by registering clients with HttpClientFactory, and injecting the interface into a class constructor.

Let’s assume we have an API for interacting with users, along with a Refit interface:

public interface IUsersClient
{
    [Get("/users")]
    Task<IEnumerable<User>> GetUsers();
}

First, we can instantiate the client using the RestService class:

var usersClient = RestService.For<IUsersClient>("https://myapi.com");
var users = await usersClient.GetUsers();

We can also register the client with HttpClientFactory provided by ASP.NET Core:

services
    .AddRefitGeneratedClient<IUsersClient>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://myapi.com"));

The registration extension lives in the Refit.HttpClientFactory package and is called AddRefitGeneratedClient<T>(). It is an extension on IServiceCollection that returns an IHttpClientBuilder, so ConfigureHttpClient() and any handler chain hang off it exactly as they would off a plain typed client. One detail worth remembering: unlike the older AddRefitClient<T>(), it is declared only on IServiceCollection, so we call it on services rather than chaining it onto an existing AddHttpClient() builder.

The name matters on Refit 15. AddRefitGeneratedClient<T>() resolves the implementation the source generator emitted at build time, while AddRefitClient<T>() asks for the reflection request builder, which moved out of the Refit package into an opt-in Refit.Reflection package. If an interface is one the generator cannot build inline, adding Refit.Reflection brings AddRefitClient<T>() back; every interface in this article generates inline, so we do not need it.

Both of these are valid ways to register and use Refit clients.

However, if we want to make our code more maintainable and testable, registering the client with HttpClientFactory and injecting it into the required class constructors is the way to go. This allows us to easily inject a mock of the interface for testing purposes, without having to rely on any of the implementation details of either HttpClient or the Refit library.

Because of this, we will opt for the latter method for the rest of this article.

Setting up an API

Instead of setting up a new API from scratch, we can use JSONPlaceholder. It is a free, fake API that can be used for testing, and fits our needs perfectly. It provides various resources to interact with, but for this demo, we’ll use the users resource.

Creating Console Application

With our API solution chosen, let’s create a console application, either through the Visual Studio template or by using dotnet new console.

We must also add the Refit library from NuGet. As we will be using the HttpClientFactory registration method, we need to add two packages:

  • Refit 15.2.0
  • Refit.HttpClientFactory 15.2.0

Both are MIT-licensed, and they always ship together, so we pin them to the same version.

As we have chosen the users resource, we’ll create a User model:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;

    public override string ToString() =>
        string.Join(Environment.NewLine, $"Id: {Id}, Name: {Name}, Email: {Email}");
}

We override the ToString() method so we can easily display the users retrieved from the API in the console.

Now we can create our Refit interface.

Implementing Refit Client

We start by creating an interface and defining a GetAll method:

public interface IUsersClient
{
    [Get("/users")]
    Task<IEnumerable<User>> GetAll();
}

To turn this interface into a Refit client, we add the Get attribute to the GetAll() method, and define the route as /users. As the API will return us a list of users, the method return type is an IEnumerable<User>.

This is enough to get us started.

Consuming API Data

As we’ve opted to register our Refit client with the ASP.NET Core dependency injection framework, we need to add the Microsoft.Extensions.Hosting 10.0.0 NuGet package to our console application.

With this done, let’s register the Refit client in the Program class:

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((_, services) =>
    {
        services
            .AddRefitGeneratedClient<IUsersClient>()
            .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/"));
    }).Build();

We use the AddRefitGeneratedClient() extension method to register the IUsersClient interface, and then configure the HttpClient, setting the BaseAddress to the JSONPlaceholder address.

With our service registration complete, we can retrieve an instance of IUsersClient, and retrieve some users:

var usersClient = host.Services.GetRequiredService<IUsersClient>();
var users = await usersClient.GetAll();

foreach (var user in users)
{
    Console.WriteLine(user);
}

First, we retrieve an IUsersClient from the service collection, and call the GetAll() method to retrieve a list of users, which we then print to the console.

This demonstrates how simple it is to use a Refit client to abstract HTTP calls. We make a method call that returns our populated User model.

Next, let’s explore some of the further capabilities of Refit, by adding more methods to IUsersClient.

Extending IUsersClient

Let’s add some basic CRUD (Create, Read, Update, Delete) operations for our API:

public interface IUsersClient
{
    [Get("/users")]
    Task<IEnumerable<User>> GetAll();

    [Get("/users/{id}")]
    Task<User> GetUser(int id);

    [Post("/users")]
    Task<User> CreateUser([Body] User user);

    [Put("/users/{id}")]
    Task<User> UpdateUser(int id, [Body] User user);

    [Delete("/users/{id}")]
    Task DeleteUser(int id);
}

Firstly, we add the GetUser() method, which takes an id parameter to identify the user we want to retrieve. We decorate this method with the Get attribute, and in the route we define a dynamic parameter using { and }.

Next up is the CreateUser() method, which takes a User as a parameter, and because we want this to be passed in the HTTP request body, we decorate the parameter with the Body attribute. This time, it’s a Post request that the API expects.

To update a user, we need a Put method, combining both a route parameter, id, and body content, which is the User we want to update.

Finally, to delete a user, we make a Delete request providing the id of the user to delete.

This gives us CRUD functionality on the Users API. Now we can test this out.

Testing CRUD Functionality

Back in the Program class, let’s start by creating a new user:

var user = new User
{
    Name = "John Doe",
    Email = "[email protected]"
};

var usersClient = host.Services.GetRequiredService<IUsersClient>();
var userId = (await usersClient.CreateUser(user)).Id;

Console.WriteLine($"User with Id: {userId} created");

Initially, we create a new User object. With this user, we call CreateUser(), which will return a User object, giving us the Id of the newly created user, which we log to the console.

Next, we can retrieve an existing user using the GetUser() method:

var existingUser = await usersClient.GetUser(1);

With this user, let’s update the Email:

existingUser.Email = "[email protected]";
var updatedUser = await usersClient.UpdateUser(existingUser.Id, existingUser);

Console.WriteLine($"User email updated to {updatedUser.Email}");

Here, we use the UpdateUser() method, passing in the Id of the user, along with the updated user object.

The final step is to delete the user:

await usersClient.DeleteUser(userId);

We simply call DeleteUser(), providing the userId to delete.

This covers the basic CRUD functionality and shows how simply we can create an interface to interact with an API, without the need of handling complex HTTP logic with an HttpClient.

How Do We Handle Errors With Refit?

By default, a failed request throws. Refit checks the response status and, on anything unsuccessful, raises an ApiException carrying the status code, the request, and the raw response content.

That is usually what we want inside a service, and rarely what we want at the edge of an application, where a 404 is a normal outcome rather than an exceptional one.

For those cases we change the return type instead of catching. Declaring the method as Task<ApiResponse<User>> makes Refit return the response object rather than throw, so we inspect IsSuccessStatusCode, read Error, and decide: no exception, no stack unwinding on an expected path.

The two styles mix freely inside a single interface. Endpoints where a failure means something is genuinely broken keep the plain return type and throw; endpoints where a failure is itself a valid answer return ApiResponse<T> and let the caller decide what to do next.

The switch is one word in the interface:

[Get("/users/{id}")]
Task<ApiResponse<User>> GetUserResponse(int id);

And the call site asks the response rather than guarding with a try block:

var response = await usersClient.GetUserResponse(999);

if (!response.IsSuccessStatusCode)
    Console.WriteLine($"Failed: {response.StatusCode}");

JSONPlaceholder has no user 999, so it answers with a 404 and our console prints the status without an exception ever being thrown:

Failed: NotFound

When we do want the exception, ApiException is the type to catch, and it carries more than a message: the status code, the request that produced it, and the raw response body, which is often where an API puts the reason. That makes it a natural place to hook retrying failed requests with Polly for the failures that are worth another attempt.

How Do We Add Headers, Query Strings, and Cancellation?

All three are declared on the interface, the same way the route is.

A fixed header goes on the method or the whole interface with [Headers]. A header whose value changes per call becomes a parameter marked [Header] (an access token, a correlation id), and Refit sets it on that request only.

Query-string values are just parameters. Anything not matched to a {placeholder} in the route is appended to the query string, and [Query] or [AliasAs] controls the name when the C# name and the API’s name differ.

Cancellation needs no attribute at all. Adding a final CancellationToken parameter is enough; Refit passes it through to the underlying request, so the call cancels the way any other HttpClient call does.

Everything cross-cutting (retries, logging, tracing) belongs in a DelegatingHandler on the registered HttpClient, not in the interface.

AttributeGoes onWhat it does
[Get], [Post], [Put], [Patch], [Delete], [Head]MethodSets the HTTP verb and the relative path
[Body]ParameterSerializes the parameter into the request body
[Query]Parameter or propertyAdds the parameter to the query string
[AliasAs]Parameter or propertyRenames the parameter in the URL or form
[Header]ParameterSets a header from an argument at call time
[Headers]Interface or methodSets fixed headers for every call
[Authorize]ParameterMarks the argument carrying the token, and sends it as an Authorization header of the given scheme
[Multipart]MethodSends the request as multipart/form-data
[Property]Parameter or propertyAttaches a value to the request for handlers to read

All three appear together in a single signature:

[Headers("User-Agent: CodeMaze-Sample")]
public interface IUsersClient
{
    [Get("/users")]
    Task<IEnumerable<User>> GetAll([Query] string? name, CancellationToken token);
}

The [Headers] attribute on the interface applies to every call it declares, name becomes ?name=... because nothing in the route claims it, and the trailing token is recognised for what it is rather than serialized into the query string. Passing an already-cancelled token throws OperationCanceledException before the request leaves, exactly as it would when cancelling a request with a CancellationToken on a plain HttpClient.

For the cross-cutting work, the pipeline is the place. Extending HttpClient with delegating handlers is how we add request logging, tracing, or an authorization header sourced from a token service, and because AddRefitGeneratedClient<T>() returns an IHttpClientBuilder, a handler attaches to a Refit client the same way it attaches to any other typed client.

Conclusion

In this article, we’ve learned how we can abstract interaction with HTTP-based APIs by using Refit and creating a simple interface for our API. This allowed us to avoid dealing with complex HTTP logic, such as creating request messages and deserializing responses and instead focus on the core logic relating to our applications.

If we are starting from an OpenAPI document rather than from scratch, Refitter is a CLI tool that generates these Refit interfaces for us, which is worth knowing about before hand-writing a large API surface.

And if we are still choosing a client library, it is worth reading how HttpClient compares with RestSharp before settling on one.

Tested with .NET 10.0.10.