As I have written in my previous posts, we are working on a greenfield solution and developing multiple microservices as part of it. Most of these microservices are Rest APIs built on ASP.NET Core. The consumers for most of these microservices are mostly other internal services, again built on ASP.NET Core.
Where we started
Now, when we started our development there were not many consumers of our microservices. Things were still simple. We simply exposed the Swagger specification along with some guidelines on how to consume the API. This was enough for anyone who needs to consume our API. We did not feel the need to spend effort in creating a client library for our API. Each consumer had their own “version” of integration Rest client to talk to our API. The consumers would auto-generate methods and models through tools such as NSwagger or sometimes simply duplicate the models and create custom Typed HttpClient to talk to our API. There was not one single consistent way of talking to our Rest service.
The complexity of our system increased over a period of time. We had multiple consumers for our API. There were cross-cutting concerns such as authentication, authorization, custom headers, logging, etc. which every client needed to adhere to. The models/ requests became complex. It started to become difficult for each client to keep the pace of new features or the changes introduced to our API. We had some healthy discussions within our team and we felt the need of exposing a client library as a NuGet package along with our Rest API.
The concept of the client library is not new. It has been around forever in some form or the other. For example, at the time of web services, we had WSDL document to describe the web service. We could auto-generate the client methods to make a call over the wire. There are also plenty of examples of the client libraries (or SDKs) from tech gaints such as Microsoft, Amazon, Google, etc.
Why client library?
Here are the reasons why we felt the client library was a good choice for us:
A consistent way of calling the API
One of the first important wins we had with the client library was that it gave clients a single and consistent way of calling our API. It allowed us to get rid of different client shims and replace them with one single implementation.
With the client library, we could to do a basic model validation even before the client request hit the server. This helped save a precious HTTP call and provide faster feedback to the client.
The client library provided us with the ability to handle versioning better. Since, we controlled the client library we could Obsolete a method, provide a new implementation, have different overloads for the same REST endpoint. For the client, it was a simple method call but under the hood, we were able to format the request any way we liked. Since all the consumers we have are internal application it was easier for us to control when the client upgrades to the newer package.
The client library provided us a consistent way of handling cross-cutting concerns such as authentication, authorization, and logging. The client simply had to inject our “Typed” client service and we were able to hook authentication token, correlation id, custom headers, etc. to the request through HttpHandlers.
The client library provided us with an opportunity to have a consistent and correct caching strategy for the Rest service. As the API owner, we had an understanding of the implementation details of the API. This gave us a way to answer questions such as what should be the cache duration, when the cache should be invalidated or most importantly does it need to be cached?
Resilience and Fault Tolerance
The client library provided us with the way of defining retry policies, timeout and implementing circuit-breaker pattern. This helps to improve the resiliency of our application. We could use a framework such as Polly under the hood without leaking the details to the client.
Change implementations under the hood
Having the client library before our API client provided us a wrapper to the REST Client. It gives ways to change our implementation without impacting our clients. Hypothetically, we could use SignalR, gRPC or mix of these but still provide a single interface for the client to call our services.
Is it a silver bullet?
Providing a Nuget package to clients to consume our Rest APIs enabled them to call our services in a simple and consistent manner. However, I would still warn against creating the client libraries for every Rest API you have.