One of the critical points, in my experience, is the API responses between different services.
Table of Contents
1 - Why unify API responses?
Once you move to microservices and have many services, having a clear and shared approach to how things are done becomes crucial.
While it is true that in microservices there's the idea that each team can do as they wish, in practice, this can be a very big problem. Ideally, you want freedom, but with certain limitations.
For example, if you use asynchronous communication, it's best to use the same service bus across all services, not just whichever you like.
The same goes for our API responses. If we respond in every service using the same logic, it will be much easier for any other service consuming ours, whether they are our own services or external clients.
2 - Unifying API responses in C#
In this post, we’ll work with code from Distribt and apply the logic of Railway Oriented Programming
not for domain logic, but only for the API response.
I have a video on ROP here. What you need to know is that the response of our API is going to be a ResultDto<T>
which will contain the following information:
public class ResultDto<T>
{
public T Value { get; set; }
public ImmutableArray<ErrorDto> Errors { get; set; }
public bool Success => this.Errors.Length == 0;
}
As you can see, we have an error list
, then T
as our generic type which will change, and then success
which tells us if an object has any errors or not.
Therefore, the API will always return the same structure and the same logic.
But it doesn't end there, my ROP library also lets us specify a Status Code
in a response
.
To do this in our code, we need to import the following NuGet package:
This will also add NetMentor.ROP. You can find the library code on GitHub.
This library allows us to use the Result structure and add status codes to your API responses.
2.1 - Which Status Code to use for API responses
We should use the status code that best fits our needs from the status code list. Some are much more common than others, and from my personal experience these are the most used:
A - Successful responses
200 OK
: This means that whatever action was taken succeeded, e.g., when you do a GET, you usually return a 200 status.201 Created
: Common when you do a POST and create a record, process, file, etc., in the system.202 Accepted
: This is linked to asynchronous processes, when you make a request, the system accepts and stores it, but the action itself hasn’t yet completed. For example, when you hit “send a product”, you get an instant response, but someone in the warehouse needs to pack and ship it, and only once that’s done is it truly completed. You should use the same logic if an API call takes a long time. Sometimes it takes extra time, sometimes not.
B - Unsuccessful responses
400 Bad Request
: Used when the server can't understand what you’re asking for. When in doubt, this is often used.404 Not found
: When you request information that isn’t available, such as a non-existent ID.409 Conflict
: When the request conflicts with the system, e.g., if you try to create two elements with the same ID, or if a unique property already exists in the database, such as a passport number.422 Unprocessable Entity
: Used when the server understands what you're sending but it contains an error, for example, validation errors. (Note: Not available in NetStandard 2.0).
I didn’t include 401 (unauthorized), 403 (forbidden), or 50x codes, since those errors are provided automatically by the framework.
2.2 - Adding HttpStatusCode to the API response
The way to include a HTTPStatusCode
in the API response is by modifying the request, but in C# you can access the ObjectResult
object which lets you change the status code.
But if you use the NetMentor.ROP.ApiExtensions library, it all becomes much smoother.
- This post is not about the library’s internals, but about the user’s final result, so if you visit the code you’ll find a full implementation, but here we’ll just see how to return a status code simply.
A - Successful Response
First, let's look at a successful response. When we return a Result<T>
from the API, we call .ToActionResult()
which creates an IActionResult
(under the hood it's a ResultDto<T>
) with an HTTP status code of 202. Now, what if we want to return another status code, like 200?
It’s very simple, before calling .ToActionResult()
you just call .UseSuccessHttpStatusCode
which lets you specify a status code if the process is successful:
public async Task<IActionResult> CreateOrder(CreateOrderRequest createOrderRequest,
CancellationToken cancellationToken = default(CancellationToken))
{
return await _createOrderService.Execute(createOrderRequest, cancellationToken)
.UseSuccessHttpStatusCode(HttpStatusCode.Created)
.ToActionResult();
}
And, as you can see in the response, it contains the status code and the structure we need.
B - Unsuccessful responses
For unsuccessful responses, the process is similar. We need our happy path just like for a successful response. The difference here is that an unsuccessful response is generated if an error occurs somewhere in the chain.
So, when you call .ToActionResult()
the library will check if there are any errors in your object and set the HttpStatusCode
depending on the type of error you’ve created:
private async Task<Result<OrderDetails>> GetOrderDetails(Guid orderId,
CancellationToken cancellationToken = default(CancellationToken))
{
OrderDetails? orderDetails = await _orderRepository.GetById(orderId, cancellationToken);
if (orderDetails == null)
return Result.NotFound<OrderDetails>($"Order {orderId} not found");
return orderDetails;
}
And as you can see, it returns a response with status code 404 when you use not found.
2.3 - Benefits of unifying API responses
The great benefit is that consumers of the API know how it works, and all APIs work the same way. This is something many companies miss, letting everyone do as they like, but API configuration is really important.
3 - Decorating the API response with OpenAPI
Of course, don't forget to decorate the endpoints with the different options so that Swagger can recognize the possibilities and add them to the OpenApi file.
[HttpGet("{orderId}")]
[ProducesResponseType(typeof(Result<OrderResponse>), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(Result<OrderResponse>), (int)HttpStatusCode.NotFound)]
public async Task<IActionResult> GetOrder(Guid orderId)
=> await _getOrderService.Execute(orderId)
.UseSuccessHttpStatusCode(HttpStatusCode.OK)
.ToActionResult();
And this is the result visible in Swagger, generated by Open API:
Conclusion
In this post we have seen the importance of unifying API responses
How to create unified APIs in C#
If there is any problem you can add a comment bellow or contact me in the website's contact form