Skip to content
ExponaBLOG
.NET & ASP.NET CORE · DAY 050 / 100

OpenAPI in .NET 9+: a practical guide for developers

Swashbuckle left the template. Here is the new setup.

Expona Admin7 min read

OpenAPI in .NET 9+: The Shape of API Documentation Just Changed

For years, if you wanted OpenAPI documentation in ASP.NET Core, you reached for Swashbuckle. It worked well enough, but it was always a third-party dependency you had to manage and configure. With .NET 9, that story shifted—Microsoft built OpenAPI generation directly into the framework. OpenAPI in .NET 9+ now means you get spec generation out of the box, lighter middleware, and better integration with the tooling ecosystem. If you've been shipping APIs without proper documentation, or if you've felt friction every time you updated Swashbuckle, this is worth your attention.

Built-in Document Generation: No More Third-Party Middleware

The biggest shift is that Microsoft.AspNetCore.OpenApi generates the specification at build time or runtime without you installing Swashbuckle. This is a significant quality-of-life improvement.

Here's the minimal setup:

csharp
var builder = WebApplicationBuilder.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/products/{id}", (int id) => new { Id = id, Name = "Widget" })
    .WithName("GetProduct")
    .WithOpenApi();

app.Run();

That's it. Call AddOpenApi() in the service container, call MapOpenApi() to expose the spec at /openapi/v1.json, and add .WithOpenApi() to any endpoint you want documented. No NuGet references to Swashbuckle, no generator configurations, no middleware ordering concerns.

The generated JSON follows OpenAPI 3.0 and 3.1 standards, so it's compatible with every tool in the ecosystem. I've tested this with frontend teams who feed it straight into code generators, and it works seamlessly.

One practical note: MapOpenApi() should stay in the development block. You don't ship the spec to production unless you have a specific reason—it's useful for internal tooling, staging environments, and local development, but publicly exposing your API structure isn't always wise.

Pick a UI: Scalar or Swagger UI

A JSON file isn't human-friendly. You need a UI. The framework doesn't ship with one, but you have solid options.

Swagger UI (Familiar, Battle-Tested)

If your teams already know Swagger UI, stick with it. Install Swashbuckle.AspNetCore.SwaggerUI (version 6.4+) and configure it to read from the built-in spec:

csharp
var builder = WebApplicationBuilder.CreateBuilder(args);

builder.Services.AddOpenApi();
builder.Services.AddSwaggerUI(options =>
{
    options.SwaggerEndpoint("/openapi/v1.json", "My API v1");
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.UseSwaggerUI();
}

app.Run();

This gives you the familiar Swagger UI interface at /swagger/index.html, but the spec is generated by the framework, not Swashbuckle. You're getting the lightweight generation without the full middleware stack.

Scalar (Modern, API-Focused)

Scalar is a modern alternative with a sleaner design and better mobile support. It's also lighter weight. Install the NuGet package and point it at your spec:

csharp
var builder = WebApplicationBuilder.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

app.Run();

Scalar serves at /scalar/v1 by default. The interface is cleaner, request/response examples are highlighted better, and I've noticed frontend teams prefer it for interactive testing.

Which one? If your organization is invested in Swagger, stay with it. If you're starting fresh or open to change, Scalar feels more modern and performs better in my experience.

Describe Endpoints: Turn a Bare Spec into Useful Documentation

A generated spec without descriptions is useless. Three methods turn a skeleton into documentation that frontend teams actually use.

WithSummary and WithDescription

These set the title and detailed explanation in the OpenAPI document:

csharp
app.MapGet("/products/{id}", GetProductById)
    .WithName("GetProduct")
    .WithSummary("Fetch a single product")
    .WithDescription("Retrieve full product details including inventory, pricing, and related items. Returns 404 if the product ID does not exist.")
    .Produces(StatusCodes.Status200OK)
    .Produces(StatusCodes.Status404NotFound)
    .WithOpenApi();

async Task GetProductById(int id, ILogger logger)
{
    logger.LogInformation("Fetching product {ProductId}", id);
    // implementation
    return Results.Ok(new Product { Id = id, Name = "Widget" });
}

In the UI, the summary appears as the operation title, and the description shows in the details panel. This is where you explain business logic, edge cases, and retry behavior.

Produces: Document Your Response Types

Produces tells the spec generator what status codes and response types your endpoint returns. The UI displays these as tabs, and code generators use them to create typed clients:

csharp
app.MapPost("/orders", CreateOrder)
    .WithSummary("Create a new order")
    .WithDescription("Creates an order and queues it for fulfillment.")
    .Produces(StatusCodes.Status201Created)
    .Produces(StatusCodes.Status400BadRequest)
    .Produces(StatusCodes.Status401Unauthorized)
    .WithOpenApi();

Now the spec includes all three response types. A frontend developer reading this knows exactly what they're working with, and a code generator produces a client method that returns one of those three outcomes.

Example: A Real Endpoint

Here's a complete, documented endpoint as I'd write it for production:

csharp
app.MapGet("/api/customers/{customerId}/invoices", GetCustomerInvoices)
    .WithName("GetCustomerInvoices")
    .WithSummary("List invoices for a customer")
    .WithDescription(
        "Returns paginated invoices for the specified customer. " +
        "Results are sorted by invoice date (newest first). " +
        "Only invoices visible to the authenticated user are returned.")
    .WithOpenApi()
    .Produces>(StatusCodes.Status200OK)
    .Produces(StatusCodes.Status404NotFound)
    .Produces(StatusCodes.Status401Unauthorized)
    .RequireAuthorization();

async Task GetCustomerInvoices(
    int customerId,
    [FromQuery(Name = "page")] int page = 1,
    [FromQuery(Name = "pageSize")] int pageSize = 20,
    IInvoiceService invoiceService = null)
{
    var result = await invoiceService.GetInvoicesAsync(customerId, page, pageSize);
    return Results.Ok(result);
}

The UI now shows the operation, all three possible responses, authorization requirements, and query parameters. A frontend developer—or a code generator—has everything needed.

Generate Clients: Never Let Types Drift Again

The real value of OpenAPI isn't documentation. It's that you can feed the spec to a code generator and get a strongly-typed client. No more manual API wrapper classes that fall out of sync with the backend.

NSwag: Full-Featured, Mature

NSwag generates C# clients and covers every OpenAPI feature. If you have a .NET frontend (WPF, WinForms, MAUI), this is the right choice:

csharp
dotnet tool install NSwag.ConsoleCore --global

nswag openapi2csharp /input:http://localhost:5000/openapi/v1.json /output:Generated/ApiClient.cs /namespace:MyApp.Api

You get a generated ApiClient class with typed methods for every endpoint. Call it in your UI code with autocomplete and compile-time safety.

Kiota: Microsoft's Official Tool

Kiota is Microsoft's answer, designed for modern SDK generation. It supports C#, TypeScript, Python, Go, Java, and more. For projects with mixed-language teams, it's a good fit:

csharp
kiota generate -l csharp -c MyApiClient -n MyApp.Api -d ./openapi/v1.json

The generated client is lighter weight than NSwag and integrates cleanly with dependency injection.

openapi-typescript: For Frontend

If your frontend is TypeScript/React, openapi-typescript generates types and a fetch-based client:

csharp
npm install openapi-typescript

openapi-typescript http://localhost:5000/openapi/v1.json -o api-types.ts

Now your React components have typed API methods with full intellisense. When you add a new field to a backend response, TypeScript catches it immediately in the UI build.

Integration in CI/CD

The real win is automating this. Add a step to your CI pipeline that fetches the spec and regenerates clients whenever the API changes:

csharp
// .github/workflows/api-client-generation.yml
- name: Generate TypeScript Client
  run: |
    npm install openapi-typescript
    openapi-typescript http://staging-api.internal/openapi/v1.json -o src/generated/api.ts
- name: Commit Changes
  run: |
    git config --global user.email "bot@company.com"
    git config --global user.name "API Bot"
    git add src/generated/api.ts
    git commit -m "Regenerate API client" || true
    git push

Every time the API spec changes, the client types update automatically. Frontend and backend stay in sync without manual work.

Common Mistakes

  • Forgetting .WithOpenApi(): If you don't add this to your endpoint, it won't appear in the spec. I've seen developers spend an hour wondering why their endpoint didn't show up in the UI.
  • Documenting Nothing: A bare spec with no summaries or descriptions defeats the purpose. Spend five minutes writing good descriptions. Frontend teams will thank you.
  • Not Calling MapOpenApi() in the Right Place: If it's outside the development environment check, you're exposing your schema to the internet. Keep it behind if (app.Environment.IsDevelopment()) or explicit middleware authorization.
  • Ignoring Produces(): Without response type documentation, code generators can't create proper clients. Always specify status codes and response shapes.
  • Assuming Your Spec is Always Up-to-Date: The spec is generated at runtime, not build time. If you change an endpoint but forget to restart the app, the old spec is still served. In CI/CD, build and test before generating clients.

Key Takeaways

  • .NET 9's built-in OpenAPI removes the Swashbuckle dependency for spec generation, simplifying your stack and reducing middleware overhead.
  • Pair the spec with Swagger UI or Scalar for interactive documentation in development environments only.
  • Use WithSummary, WithDescription, and Produces to turn a bare spec into useful documentation that frontend teams actually read.
  • Feed your OpenAPI spec to NSwag, Kiota, or openapi-typescript to generate typed clients, eliminating manual API wrapper classes and keeping types in sync across your stack.
  • Automate client generation in your CI/CD pipeline so frontend and backend types never drift when you make API changes.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles