Skip to main content

Creating an API SDK

In a distributed system it is very common for services to need to talk to each other to retrieve data that they might not have readily available. For example, if the main API needs access to the user's cart in order to create an order and grant enrollments, the main API will need to call the Cart API instead of going straight to the cart database.

It would be very inconvenient if every consumer needed to implement their own logic to access every single API that they might need to use to access data.

That's why, it is very common for API owners to offer SDKs for other services to use. In this section we will learn how to create API SDKs easily.

Since the Cart API will need information from the Main API, we are going to create an SDK for the Main API.

Project structure

The idea is that the SDK is going to be an independent shippable Nuget package. For that reason it is going to live in its own assembly. The naming convention I like to use is {ProjectName}.Sdk. This however introduces a new problem.

The consumer should know what type is returned by each endpoint. This means that our API Request and Response types need to be exposed. It is generally considered a bad practice to expose our Domain objects because those can change and break our API consumers. What we are going to do instead if move our Request and Response contracts into their own assembly and refer to that project from both the SDK and the API. This poses the problem of having API contracts and domain objects. To solve that we are going to create a mapper.

We will do that without using any library.

First let's create a new project named Dometrain.Monolith.Api.Contracts.

Then let's create a folder called Responses and move the StudentRegistrationRequest record in it.

Then let's create the StudentResponse type:

public class StudentResponse
{
public required Guid Id { get; init; }

public required string Email { get; init; }

public required string FullName { get; init; }
}

And finally let's update the mapper to include a mapping to the response type:

public static StudentResponse? MapToResponse(this Student? student)
{
if (student is null)
{
return null;
}

return new StudentResponse
{
Id = student.Id,
Email = student.Email,
FullName = student.FullName
};
}

And update the StudentEndpoints file.

Now let's do the same for the Courses type.

Now that we have the contracts and mapping sorted, let's create the SDK project and create our API Client.

The old-school way: HttpClient

The simplest way to create an API client is to use the HttpClient directly.

There are two sets of endpoints we are going to expose in the SDK:

  1. The Course endpoints
  2. The Student endpoints

For that reason we will create an IStudentsApiClient and an ICoursesApiClient.

public interface ICoursesApiClient
{
Task<CourseResponse?> GetAsync(string idOrSlug);
}

public class CoursesApiClient : ICoursesApiClient
{
private readonly IHttpClientFactory _httpClientFactory;

public CoursesApiClient(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}

public async Task<CourseResponse?> GetAsync(string idOrSlug)
{
var client = _httpClientFactory.CreateClient("dometrain-api");
var response = await client.GetAsync($"courses/{idOrSlug}");

if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}

return await response.Content.ReadFromJsonAsync<CourseResponse>();
}
}
public interface IStudentsApiClient
{
Task<StudentResponse?> GetAsync(string idOrEmail);
}

public class StudentsApiClient : IStudentsApiClient
{
private readonly IHttpClientFactory _httpClientFactory;

public StudentsApiClient(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}

public async Task<StudentResponse?> GetAsync(string idOrEmail)
{
var client = _httpClientFactory.CreateClient("dometrain-api");
var response = await client.GetAsync($"students/{idOrEmail}");
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}

return await response.Content.ReadFromJsonAsync<StudentResponse>();
}
}

We can demonstrate how the API SDK works by creating a Console application:

var host = Host.CreateApplicationBuilder();

host.Services.AddDometrainApi("http://localhost:5148", "ThisIsAlsoMeantToBeSecret");

var app = host.Build();

var coursesClient = app.Services.GetRequiredService<ICoursesApiClient>();
var studentsClient = app.Services.GetRequiredService<IStudentsApiClient>();

var course = await coursesClient.GetAsync();
var student = await studentsClient.GetAsync();

Console.WriteLine();

Now we can simply run it and see it working.

Even though we could leave our code as it is, we can actually take it to the next level by introducing a Nuget package called Refit that will greatly simplify our API Client code.

The mega super cool way: Refit

Don't get me wrong, I like hand writing the same code over and over again as much as the next guy, but there must be a better way.

Introducing Refit.

Let's add the nuget package Refit.HttpClientFactory and let me show you the beauty of Refit.

public interface ICoursesApiClient
{
[Get("/courses/{idOrSlug}")]
Task<CourseResponse?> GetAsync(string idOrSlug);
}

public interface IStudentsApiClient
{
[Get("/students/{idOrEmail}")]
Task<StudentResponse?> GetAsync(string idOrEmail);
}

Yep, those are our API clients.

In order to resolve them all we need to do it:

var client = RestService.For<IStudentsApiClient>(baseAddress);

And that's it. The beauty of Refit is that it knows that every API Client looks almost exactly the same so it gives us a few configuration points and then code generates the rest of the client.

In our case however we will need to configure two things.

  1. Registering the client in dependency injection
  2. Adding the API Key configuration into the mix

To register the API client all we need to do is:

services.AddRefitClient<IStudentsApiClient>()
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri(baseAddress);
c.DefaultRequestHeaders.Add("x-api-key", apiKey);
});

This will register the client properly including this backing HttpClient and the needed auth.

Let's test it in the Console Consumer!

Making our SDK resilient

Being able to wire up an HttpClient to make requests to another service is one thing but making it worthy of being put into production is a bit more complicated.

API Clients need to be resilient. This means that when there is a transient error during our call, we might want the client to automatically retry this request. There are many different types of resilient patterns for api clients, but we are going to start with the basics.

You might have heard of the Nuget package Polly. Polly provides an elegant interface and developer experience using a fluent builder to create resilience policies such as retries, circuit breakers, fallbacks and so on.

Even though we could use the standalone Polly Nuget package, we are instead going to use a new package made by Microsoft called Microsoft.Extensions.Http.Resilience. This package is a wrapper on top of Polly but focused on Http resilience.

We can now use extensions on top of ConfigureHttpClient to add resilience.

The simplest method we can use is: AddStandardResilienceHandler.

The default configuration chains five resilience strategies in the following order (from the outermost to the innermost):

OrderStrategyDescription
1Rate limiterThe rate limiter pipeline limits the maximum number of concurrent requests being sent to the dependency.
2Total request timeoutThe total request timeout pipeline applies an overall timeout to the execution, ensuring that the request, including retry attempts, doesn't exceed the configured limit.
3RetryThe retry pipeline retries the request in case the dependency is slow or returns a transient error.
4Circuit breakerThe circuit breaker blocks the execution if too many direct failures or timeouts are detected.
5Attempt timeoutThe attempt timeout pipeline limits each request attempt duration and throws if it's exceeded.

Making a custom policy

We can also make a custom policy with our own configuration. We can do that by using the AddResilienceHandler extension method.

Then we can simply define a policy:

builder.AddRetry(new HttpRetryStrategyOptions
{
Delay = TimeSpan.FromSeconds(2),
UseJitter = true,
BackoffType = DelayBackoffType.Exponential,
MaxRetryAttempts = 2,
ShouldHandle = static args => ValueTask.FromResult(args is
{
Outcome.Result.StatusCode:
HttpStatusCode.RequestTimeout or
HttpStatusCode.TooManyRequests
})
});