Unifying API Responses

One of the critical points, in my experience, is the API responses between different services. 

 

 

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.

respuesta rop api

 

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.

non-happy path rop

 

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:

open api swagger response

 

 

Conclusion

In this post we have seen the importance of unifying API responses

How to create unified APIs in C#

 

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café