Working with REST APIs in C# often means dealing with repetitive boilerplate code—creating HttpClient instances, handling requests, parsing responses, and managing exceptions. Refit, a library inspired by Square’s Retrofit for Android, eliminates that pain by turning your REST APIs into live, type-safe interfaces.
In this article, you’ll learn how to integrate Refit into your C# projects and build cleaner, more maintainable HTTP clients.
What is Refit?
Refit is a REST library for .NET that automatically generates an implementation of your API interfaces at runtime. You define an interface representing your API contract using attributes to describe HTTP methods, and Refit takes care of the rest.
Instead of writing repetitive code for GET, POST, or PUT requests, your code looks as simple as calling methods on a service.
Why use Refit?
Refit is a powerful library that simplifies working with REST APIs in C# by offering several key advantages. It eliminates repetitive boilerplate code required for creating and managing HTTP requests and responses, helping developers focus on business logic instead of plumbing. With its clean, declarative interfaces, Refit ensures greater maintainability and readability of code. It also provides compile-time type safety with strongly typed models, reducing runtime errors and improving reliability. Additionally, Refit integrates seamlessly with dependency injection in ASP.NET Core, making it an excellent choice for building scalable and maintainable API-driven applications.
Demo of Refit
This demo will walk through setting up Refit in a C# project. You’ll learn how to install it, create data models, define API interfaces, and use generated clients to interact with external services. By the end, you’ll understand how Refit automatically handles request creation, serialization, and deserialization so you can focus on writing clean, maintainable code.
Setting up the Project
Start by creating a new .NET console or web project and installing Refit.
dotnet new console -n RefitDemo
cd RefitDemo
dotnet add package Refit These commands create a new console project and add the Refit library. The Refit package gives you tools and attributes that convert your C# interfaces into automatically generated REST clients.
Creating a Data Model
Every API returns data structures that you must map into C# objects. Define a simple model that represents the structure of the data you’ll receive from the API.
public class Post
{
public int UserId { get; set; }
public int Id { get; set; }
public string Title { get; set; }
public string Body { get; set; }
} This class represents a single post resource, matching how data is returned from the JSONPlaceholder API. Refit will serialize and deserialize JSON into this model whenever you make requests.
Defining the API Interface
Next, describe your REST API using an interface decorated with Refit attributes. Each method in the interface maps directly to an HTTP endpoint.
using Refit;
using System.Collections.Generic;
using System.Threading.Tasks;
public interface IJsonPlaceholderApi
{
[Get("/posts")]
Task<IEnumerable<Post>> GetPostsAsync();
[Get("/posts/{id}")]
Task<Post> GetPostByIdAsync(int id);
[Post("/posts")]
Task<Post> CreatePostAsync([Body] Post newPost);
} Here’s what’s happening:
[Get("/posts")]declares a GET request to the/postsroute.[Get("/posts/{id}")]dynamically replaces{id}with the method argument when making the call.[Post("/posts")]describes a POST endpoint where[Body]tells Refit to send the Post object as JSON.
Refit uses this interface to auto-generate a concrete implementation that handles HTTP communication internally. This removes the need to write repetitive HttpClient logic yourself.
Making API Calls
Now that your interface is defined, you can create a Refit client and start calling these endpoints as ordinary C# methods.
using Refit;
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// Creates an instance of the API client for the given base URL
var api = RestService.For<IJsonPlaceholderApi>("https://jsonplaceholder.typicode.com");
// Fetches all posts via a GET request
var posts = await api.GetPostsAsync();
Console.WriteLine($"Fetched {posts.Count} posts from the API.");
// Creates a new post using a POST request
var newPost = await api.CreatePostAsync(new Post
{
UserId = 1,
Title = "Refit Demo",
Body = "This is a sample post created using Refit."
});
Console.WriteLine($"Created a new post with ID: {newPost.Id}");
}
} This code sends HTTP requests under the hood, but you never manually call HttpClient or handle JSON serialization. Refit automatically:
- Builds an
HttpClientfor the base URL. - Converts your
Postobject into JSON for uploads. - Maps the JSON responses back into strongly typed
Postinstances.
Integrating Refit with Dependency Injection
In many modern .NET applications, you’ll want to use your API client as a shared service that can be easily reused across different classes. Refit integrates smoothly with .NET’s dependency injection (DI) system, making this setup straightforward whether you’re building a console app, background service, or web application.
To begin, register your Refit client with the service collection during application startup:
using Microsoft.Extensions.DependencyInjection;
using Refit;
var services = new ServiceCollection();
services.AddRefitClient<IJsonPlaceholderApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"));
var serviceProvider = services.BuildServiceProvider(); This code registers your Refit-generated HTTP client with the dependency injection container. When you build the service provider, the DI system automatically creates and manages the lifetime of your API client, ensuring that it remains efficient and reusable throughout the application.
You can then inject your API interface into any service or class that needs to make API calls:
public class PostsService
{
private readonly IJsonPlaceholderApi _api;
public PostsService(IJsonPlaceholderApi api)
{
_api = api;
}
public async Task<IEnumerable<Post>> GetAllPostsAsync()
{
return await _api.GetPostsAsync();
}
} This setup works in any .NET environment that supports dependency injection, including console applications, worker services, desktop apps, and web APIs. It provides a clean separation of concerns by isolating external API logic from your application’s business logic, which makes your code easier to maintain and test.
Understanding How Refit Works Internally
Refit relies on reflection and dynamic code generation. When you call RestService.For<IJsonPlaceholderApi>(), it inspects your interface, reads the attributes, and builds an HTTP client that:
- Handles serialization and deserialization using
System.Text.JsonorNewtonsoft.Json. - Manages path and query parameters automatically.
- Adds headers and request bodies as defined by your attributes.
This happens only once per interface and provides a lightweight but powerful abstraction over HttpClient.
Advanced Usage
Once you are comfortable with Refit’s basic setup, you can start taking advantage of its powerful advanced features. These capabilities make it easy to adapt Refit clients for authentication, file uploads, complex parameters, and network resilience.
Custom Headers
Adding headers to your HTTP requests is a common need, particularly for authentication, content type, or custom metadata. Refit supports this with the [Headers] attribute at both the interface and method level.
[Headers("Authorization: Bearer")]
public interface IProtectedApi
{
[Get("/users/profile")]
Task<UserProfile> GetUserProfileAsync();
} You can also define dynamic headers by including them in method parameters. For example, if your token changes at runtime:
[Get("/users/profile")]
Task<UserProfile> GetUserProfileAsync([Header("Authorization")] string token); This way, you can pass headers dynamically per request without rebuilding the Refit client.
AliasAs for Parameter Mapping
The [AliasAs] attribute allows you to rename parameters when they differ between your C# naming convention and the API’s expected query parameters or fields. For instance, an API might expect user_id, while your model property uses UserId.
public class UserRequest
{
[AliasAs("user_id")]
public int UserId { get; set; }
[AliasAs("include_details")]
public bool IncludeDetails { get; set; }
} When sent as a request payload or query, Refit uses the aliased names so the server receives parameters in the expected format.
File Uploads with Multipart Requests
Refit makes handling file uploads straightforward using the [Multipart] and [AliasAs] attributes. You can create multipart form-data requests for file and additional fields combined.
public interface IFileApi
{
[Multipart]
[Post("/upload")]
Task<ApiResponse> UploadFileAsync(
[AliasAs("file")] StreamPart file,
[AliasAs("description")] string description);
} In this example:
[Multipart]indicates the method uses multipart/form-data encoding.StreamPartwraps a file stream along with its content type and file name.[AliasAs]maps each parameter to the corresponding form field name expected by the API.
To use it, you would call:
var fileStream = File.OpenRead("data.pdf");
var filePart = new StreamPart(fileStream, "data.pdf", "application/pdf");
await api.UploadFileAsync(filePart, "Project Report"); Refit handles serialization of the multipart content automatically.
Integrating with Polly for Resilient Networking
Network instability or transient errors are common when making external API calls. Polly is a .NET resilience library that provides retry, circuit breaker, and fallback logic. You can combine it with Refit to automatically recover from temporary failures.
services.AddRefitClient<IJsonPlaceholderApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"))
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.RetryAsync(3)); This setup automatically retries failed requests up to three times before throwing an exception, which significantly improves reliability in distributed systems.
Custom JSON Serialization
Refit supports both System.Text.Json and Newtonsoft.Json for serialization. You can configure which one to use and fine-tune how data is serialized and deserialized.
For example, to use Newtonsoft.Json with custom settings:
var settings = new RefitSettings
{
ContentSerializer = new NewtonsoftJsonContentSerializer(
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.Indented
})
};
var api = RestService.For<IJsonPlaceholderApi>(
"https://jsonplaceholder.typicode.com",
settings); This is particularly useful when dealing with complex APIs that use unconventional formatting or nested objects.
Custom Request Logging and Interceptors
In some cases, you may want to inspect or log HTTP requests and responses for debugging, analytics, or monitoring purposes. Refit allows you to do this by configuring a custom HttpMessageHandler or by adding a middleware-style interceptor when creating your client.
For example, you can build a simple handler that logs outgoing requests and incoming responses.
public class LoggingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine($"Request: {request.Method} {request.RequestUri}");
if (request.Content != null)
{
var content = await request.Content.ReadAsStringAsync();
Console.WriteLine($"Request Body: {content}");
}
var response = await base.SendAsync(request, cancellationToken);
Console.WriteLine($"Response: {(int)response.StatusCode} {response.ReasonPhrase}");
return response;
}
} You can attach this handler when building your Refit client:
var api = RestService.For<IJsonPlaceholderApi>(new HttpClient(new LoggingHandler())
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com")
}); Alternatively, if you’re using dependency injection, you can integrate it directly into your client registration:
services.AddRefitClient<IJsonPlaceholderApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"))
.AddHttpMessageHandler<LoggingHandler>(); This approach allows all outgoing requests made via Refit to pass through your LoggingHandler, giving you full visibility into headers, payloads, and responses without altering the API code itself.
Why Advanced Features Matter
These advanced features allow Refit to scale from simple demos to enterprise-level API integration. Whether you need authenticated requests, multipart uploads, query parameter flexibility, or robust error handling, Refit provides concise tools to achieve all of this with minimal code.
Conclusion
Refit takes the complexity out of working with REST APIs in C#. Instead of manually writing HTTP requests, parsing responses, and handling serialization, you define declarative interfaces that map directly to your endpoints. This simplifies your codebase, increases maintainability, and ensures type safety at compile time. Whether you’re building simple API consumers or large-scale microservices, Refit provides a clean and modern approach to communicating with web services.
I hope you’ve learned something valuable from this guide and feel confident trying Refit in your own projects. It’s one of my favorite .NET libraries, and I use it all the time in my job because it makes API integration faster, cleaner, and far more enjoyable.