Making Production-Ready APIs in .NET – Part 2: A Professional Looking API

Author
By Giannis Georgopoulos ·


Series: Building "Production-Grade" APIs in .NET

  1. Intro - What is a "Production-Grade" APIBuilding “Production-Grade” APIs in .NET – IntroSeries: Building "Production-Grade" APIs in .NET 1. Intro - What is a "Production-Grade" API 1. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse Consumers 1. Part 2 - A Professional Looking API 1. Observability & Diagnostics 1. Resilience, Security & Safety 1. CI/CD & Safe Deployments Many engineers build and deploy APIs into production. So we have an API running in production — does that mean it’s truly production-grade? More often than not, the answer is no. We write the cod
  2. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse ConsumersDesigning Clean, Intuitive APIs That Don’t Confuse ConsumersSeries: Building "Production-Grade" APIs in .NET 1. Intro - What is a "Production-Grade" API 1. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse Consumers 1. Part 2 - A Professional Looking API 1. Observability & Diagnostics 1. Resilience, Security & Safety 1. CI/CD & Safe Deployments Let’s say you join a new team and find this in one of the core API controllers: [HttpPost("updateOrder")] public async Task UpdateOrder([FromBody] OrderDto order) { var updatedOrder = await _ord
  3. Part 2 - A Professional Looking APIMaking Production-Ready APIs in .NET – Part 2: A Professional Looking APISeries: Building "Production-Grade" APIs in .NET 1. Intro - What is a "Production-Grade" API 1. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse Consumers 1. Part 2 - A Professional Looking API 1. Observability & Diagnostics 1. Resilience, Security & Safety 1. CI/CD & Safe Deployments Your OpenAPI spec is the contract that defines how consumers interact with your API. It powers visual tools that help developers understand, test, and integrate with your endpoints. This spec powers
  4. Observability & Diagnostics
  5. Resilience, Security & Safety
  6. CI/CD & Safe Deployments

Your OpenAPI spec is the contract that defines how consumers interact with your API. It powers visual tools that help developers understand, test, and integrate with your endpoints.

This spec powers visual tools that help developers understand, test, and integrate with your API. While Swagger UI has been the go-to for years, modern alternatives like Scalar are raising the bar with cleaner UX, better search, and more intuitive navigation. For this post, we’ll be using Scalar to showcase how a well-designed OpenAPI spec gives your API a professional, developer-friendly front.

💡 Hint: Starting in .NET 9, there’s a built-in way to generate the OpenAPI spec using builder.Services.AddOpenApi(). However, as of now, it does not support XML comments, which are essential for rich and descriptive documentation. This makes it a no-go for production APIs that prioritize clarity. It is supported however in .NET 10 and you can see an example here.

In Part 1, we cleaned up a flawed UpdateOrder endpoint and replaced it with clear, task-based actions like SetShippingInfo. Now in Part 2, we’ll visualize the impact of those changes in our OpenAPI docs. You’ll see how thoughtful design decisions translate into:

  • Cleaner, more understandable docs

  • Easier client integration

  • Fewer support questions

  • And a stronger impression of your API as production-grade

Let’s make your OpenAPI spec do more than just validate — let’s make it teach, guide, and inspire confidence.

Visualizing the Difference: v1 vs v2

Let’s bring our changes to life by exposing two versions of the same endpoint:

  • Version 1 (v1) shows the original, flawed UpdateOrder design

  • Version 2 (v2) presents the refactored, task-based SetShippingInfo approach

This not only helps us compare the before/after in Swagger/Scalar — it also gives us a chance to introduce API versioning, a crucial concept for long-term maintainability.

Setting Up Versioning and Swagger

We’ll use URL-based versioning (/api/v1/..., /api/v2/...) since it’s the most straightforward to demonstrate in OpenAPI docs and widely supported by tools like Scalar.

First, install the necessary NuGet packages:

# API versioning for controllers
dotnet add package Asp.Versioning.Mvc 

# Adds support for exposing versioned API docs
dotnet add package Asp.Versioning.Mvc.ApiExplorer 

# Swagger/OpenAPI generation
dotnet add package Swashbuckle.AspNetCore

# Adds support for example responses in Swagger, we will use it to provide examples for `ProblemDetails` responses
dotnet add package Swashbuckle.AspNetCore.Filters

# Optional: Use Scalar instead of Swagger UI for a better experience
dotnet add package Scalar.AspNetCore

In your Program.cs:

using Asp.Versioning;  
using BloggingExamples;  
using Scalar.AspNetCore;  
using Swashbuckle.AspNetCore.Filters;  
  
var builder = WebApplication.CreateBuilder(args);  
  
builder.Services  
    .AddSwaggerGen()  
    .ConfigureOptions<ConfigureSwaggerOptions>();  
  
builder.Services.AddSwaggerExamplesFromAssemblyOf<NotFoundProblemDetailsExample>();  
  
builder.Services.AddApiVersioning(options => // adds api versioning and configures the strategy  
    {  
        options.DefaultApiVersion = new ApiVersion(1, 0);  
        options.ReportApiVersions = true;  
        options.ApiVersionReader = new UrlSegmentApiVersionReader();  
    })    .AddMvc() // add support for versioning controllers   
.AddApiExplorer(options =>  
    {  
        options.GroupNameFormat = "'v'VVV"; // e.g., v1  
        options.SubstituteApiVersionInUrl = true;  
    });  
builder.Services.AddControllers();  
  
var app = builder.Build();  
  
app.UseSwagger();  
  
app.MapControllers();  
app.MapScalarApiReference(x =>  
{  
        x.OpenApiRoutePattern = "/swagger/{documentName}/swagger.json";  
    x.AddDocument("v1");  
    x.AddDocument("v2");  
});  
  
await app.RunAsync();

In OrderController.cs:

[ApiVersion("1")]  
[ApiVersion("2")]  
[ApiController]  
[Route("api/v{v:apiVersion}/[controller]")]  
public class OrderController(OrderService orderService) : ControllerBase  
{  
    [MapToApiVersion("1")]  
    [HttpPost("updateOrder")]  
    public async Task<IActionResult> UpdateOrder([FromBody] OrderDto order)  
    {        var updatedOrder = await orderService.Update(order);  
  
        return Ok(updatedOrder);  
    }
    
    
    [MapToApiVersion("2")]  
    [HttpPatch("{orderId}/shipping")]  
    [ProducesResponseType(StatusCodes.Status204NoContent)]  
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]  
    [ProducesResponseType(StatusCodes.Status404NotFound)]  
    public async Task<IActionResult> SetShippingInfo(Guid orderId, SetShippingInfoRequest request)  
    {        await orderService.SetShippingInfo(orderId, request);  
        return NoContent();  
    }}

Now, if you launch your API and visit swagger/index.html, you’ll see both v1 and v2 grouped cleanly — and instantly appreciate the differences:

Let's take a look at v1 first.

swagger-post-v1.png

This is functional, but far from intuitive:

  • No example values provided, everything is nullable abd you’re left guessing how to structure your request

  • Errors like 400 Bad Request aren’t documented, leaving you blind to what might go wrong

  • No clear separation between request and response — just a generic 200 OK

  • It works, but it feels unpolished and leaves the developer with more questions than answers

Now lets take a look at v2 and compare them:

swagger-post-v2.png

In v2 we can see:

  • Ready-to-run example values make it easy to try out the endpoint without guesswork

  • All possible status codes are clearly documented, along with explanations

  • A structured ProblemDetails schema shows exactly how errors are returned

  • Clear separation between request and response models

  • The UI communicates confidence — this feels like a production-grade API

Wrapping Up

Improving your OpenAPI spec isn’t just about aesthetics — it’s about creating a developer experience that feels reliable, guided, and easy to integrate with.

By versioning your API and refining your endpoints, you instantly elevate the quality of your documentation. It becomes easier to test, easier to onboard new consumers, and easier to maintain over time.

In this post, we saw how small changes — like switching from a vague UpdateOrder to a specific SetShippingInfo — dramatically improve how your API looks and feels in tools like Scalar.