Nowadays, the creation of APIs is a very common solution when we want to make some features of our system available for other components. Since that, we can put an API as a public component and allow you to use it. The thing is that if you want to make some requests in your backend you must create an HTTP client in order to do the necessary requests.
Why is it good to provide a client library to your API?
That’s the main question that you want to have the right answer. It helps you to ignore some operations when you are developing a service because it avoids you from reinventing the wheel every time. Regarding this, it will increase your time to focus on your own system and not in creating the code to make HTTP requests. The client already gives you a response object ready to use instead of giving you a raw response which you must serialize for an object.
You also don’t need to know which version of an API are you using. The client abstracts you from this. If the API logic or rules change, you don’t need necessarily to update the client version if the interfaces not change, meaning this the input, output, or route of an endpoint didn’t change.
Let’s talk about code…
For this case let’s say that we have a database that contains all users and their groups from a company. There is an API that exposes that information. We have two controllers, one for users and another for groups.
[ApiController]
[Route("api/users/")]
public class UserController : ControllerBase
{
private List<User> _inMemoryUsers;
public UserController()
{
_inMemoryUsers = new List<User>()
{
new User(1, "John", "john@mail.com", true),
new User(2, "Lisa", "lisa@mail.com", true),
new User(3, "Bernard", "bernard@mail.com", false)
};
} [HttpGet("all")]
public IEnumerable<User> GetAll()
{
return _inMemoryUsers;
} [HttpGet("{id}")]
public User GetById(int id)
{
return _inMemoryUsers.SingleOrDefault(u => u.Id == id);
}
}
As you can see my “database” it’s just a list of User and Group.
[Route("api/groups")]
[ApiController]
public class GroupController : ControllerBase
{
private List<Group> _inMemoryGroups;
public GroupController()
{
_inMemoryGroups = new List<Group>()
{
new Group(1, "Manager", new []{ 1 }),
new Group(2, "Developers", new []{ 2, 3 })
};
} [HttpGet("all")]
public IEnumerable<Group> GetAll()
{
return _inMemoryGroups;
} [HttpGet("{id}")]
public Group GetById(int id)
{
return _inMemoryGroups.SingleOrDefault(u => u.Id == id);
}
}
The challenge is to provide a client interface that gives you access to all the available endpoints.
Client Architecture
We have the main interface IApiClient that contains the following definition:
public interface IApiClient
{
IUserClient UserClient { get; }
IGroupClient GroupClient { get; }
}
Its implementation is also very simple:
public class MyApiClient : IApiClient
{
public MyApiClient(IUserClient userClient,
IGroupClient groupClient)
{
UserClient = userClient;
GroupClient = groupClient;
} public IUserClient UserClient { get; }
public IGroupClient GroupClient { get; }
}
With this interface, we will have access to all of the interfaces to interact with the existing controllers. The next code block shows the interfaces from IUserClient , which knows how to interact with the /users endpoint and IGroupClient which knows something similar but regarding the /groups endpoint.
public interface IUserClient
{
Task<IEnumerable<User>> GetAll();
Task<User> GetById(int id);
}public interface IGroupClient
{
Task<IEnumerable<Group>> GetAll();
Task<Group> GetById(int id);
}
As we can see, these interface methods are very similar to the existing ones on respective controllers.
What if I add a new endpoint on my API?
For each new endpoint for any controller, we must create a new interface method on the client library side and implement it. After that, we just need to generate a new version of the client to be distributed.
So at that moment, we have:
API implemented with two controllers
A client library with two interfaces, one for each controller
The main interface which contains all the available interfaces to interact with the existing endpoints
What is the purpose of the MyHttpClient class?
The goal of this class is to simplify the work that must be done in order to make HTTP requests to our API. Instead of doing the same code for each client implementation, we create those features here and extend from it.
public class MyHttpClient
{
private readonly HttpClient _httpClient;
private const string BASE_URL = "http://localhost:5000/api/"; public MyHttpClient()
{
_httpClient = new HttpClient();
} public async Task<string> GetRequest(string url)
{
string endpointPath = BASE_URL + url;
// Make the request
HttpResponseMessage response = _httpClient.
GetAsync(endpointPath).
Result; if (!response.IsSuccessStatusCode)
throw new ArgumentException($"The path {endpointPath} gets the following status code: " + response.StatusCode); return await response.Content.ReadAsStringAsync();
}
}
To make the objective of this class clear let’s see the implementation of IUserClient :
public class UserClient : MyHttpClient, IUserClient
{
public async Task<IEnumerable<User>> GetAll()
{
string result = await GetRequest("users/all");
return JsonConvert.
DeserializeObject<IEnumerable<User>>(result);
} public async Task<User> GetById(int id)
{
string result = await GetRequest($"users/{id}");
return JsonConvert.DeserializeObject<User>(result);
}
}
How to configure the dependency injection registry?
I recommend you create a ServiceCollection extension method to configure all the necessary dependencies. With this, the users of your client just need to add this method to the initial configuration class.
public static void AddApiClient(this IServiceCollection services)
{
services.AddSingleton<IUserClient, UserClient>();
services.AddSingleton<IGroupClient, GroupClient>();
services.AddSingleton<IApiClient, MyApiClient>();
}
Note: Take care of the order of this registration because if you register the IApiClient in the first place, the application will fail because it won't know how to inject IUserClient and IGroupClient which are necessary to use IApiClient interface.
How is it to use the client library?
In this example, I will use a WorkerService project template. In the perspective of who will using your client library just need to:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args).
ConfigureServices((hostContext, services) =>
{
services.AddApiClient();
services.AddHostedService<Worker>();
});
In your Worker class you can use the IApiClient to get data from both existing clients. Let’s take a look at the ExecuteAsync method.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
IEnumerable<User> users = await _apiClient.UserClient.GetAll();
Group group = await _apiClient.GroupClient.GetById(2); (...)
}
Conclusion
In my opinion, this is a simple and efficient way to create clients for APIs. The fact of having multiple controllers doesn’t create any issue here, you just need to add and implement a new interface and declare it on the main interface of the library ( IApiClient in this example).
This post was made based on .NET Core but the architecture design can be the same for other languages. With this type of client library, you will able to provide to your users a very practical way to interact with your API, without any necessity to give direct access to it.
Source: Medium
The Tech Platform
Comentarios