{
  "$type": "site.standard.document",
  "canonicalUrl": "https://johnnyreilly.com/posts/dotnet-openapi-and-openapi-ts",
  "description": "This post will show you how to write full stack applications with static typing from back to front using OpenAPI, TypeScript and .NET.",
  "path": "/posts/dotnet-openapi-and-openapi-ts",
  "publishedAt": "2026-01-02T00:00:00.000Z",
  "site": "at://did:plc:yy3apqjlms24kso7ahn7lbmb/site.standard.publication/3mova7c4nho2b",
  "tags": [
    "swagger",
    "c#",
    "azure",
    "typescript"
  ],
  "textContent": "I've long believed in the benefits of static typing. Static typing helps you catch errors early, improves code navigation and makes refactoring easier. In recent years I've been using TypeScript on the front end and Con the back end to get these benefits. I wrote previously about how to do this with NSwag and I thought it was probably worth returning to the topic. How would I do the same thing now?\n\n\n\nNSwag is still great, but it produces OpenAPI 3.0 specifications. However, Microsoft have been working on their own OpenAPI tooling for .NET. The Microsoft.AspNetCore.OpenApi package provides functionality to generate OpenAPI specifications from ASP.NET Core Web APIs and it supports OpenAPI 3.1. This difference turns out to be significant when it comes to handling nullability.\n\nThere was a change to how nullablity is represented in OpenAPI 3.1 compared to 3.0. Whether that change is the cause or not I'm not sure, but the OpenAPI specifications produced by Microsoft.AspNetCore.OpenApi seem to surface nullability better than I've found with NSwag or Swashbuckle. If something is not defined as nullable in the Cmodel, it is not marked as nullable in the OpenAPI spec. This means that when we generate TypeScript clients from the OpenAPI spec, we get better nullability support in TypeScript too. Previously I'd find I'd do a lot of null checks or assertions in TypeScript even when the Cmodel didn't allow nulls. Now, with OpenAPI 3.1 and Microsoft.AspNetCore.OpenApi, I find that much less often.\n\nThe client that NSwag generates is also still very useful. But it is somewhat \"heavy\" in that it creates a lot of code, and it is runtime code, so it adds to my bundle size and my execution time. The alternative I'm going to show you here is to use OpenAPI TypeScript / openapi-ts. This is a lightweight TypeScript client generator for OpenAPI 3.x specifications. Most of the work it does is in the form of TypeScript type definitions. Given that type definitions are erased at runtime, the resulting client code is very lightweight. It also has good support for OpenAPI 3.1.\n\nWhat will we do?\n\nSo in this post we're going to do exactly what I did in my 2021 post, but this time using Microsoft.AspNetCore.OpenApi to generate the OpenAPI spec and openapi-ts to generate the TypeScript client.\n\nWe will:\n\n- Create a .NET app which exposes an OpenAPI endpoint with Microsoft.AspNetCore.OpenApi.\n- Create a script which, when run, creates a TypeScript client with openapi-ts.\n- Consume the API using the generated client in a simple TypeScript application.\n\nIf you're going to do this, you will need both Node.js and the .NET SDK installed.\n\nCreate an API\n\nWe'll now create an API which exposes an Open API endpoint:\n\nThe above command creates a new .NET Web API project in a folder called server. Pretty much all the code we care about is in Program.cs:\n\nThis is simply exposing a single endpoint, /weatherforecast which returns some (fake) weather data. If we run our API with:\n\nWe can then navigate to http://localhost:5000/weatherforecast and see the JSON output:\n\nAnd we can see the OpenAPI endpoint at http://localhost:5000/openapi/v1.json:\n\nThis is great! (Actually, there's some problems with the temperatureC and temperatureF properties being marked as both integer and string but we'll ignore that for now.)\n\nCreate our client\n\nWe'll now create a web app with which to consume our API:\n\nThis creates a React + TypeScript app in a folder called client. We'll now follow the openapi-ts setup instructions to add openapi-ts to our project:\n\nAnd we'll update the tsconfig.app.json to include the recommended settings:\n\nTo make local development easier, we'll also add a proxy to our vite.config.ts so that API request is proxied to our .NET API:\n\nNow we no longer need to deal with CORS during development, and our local development setup more closely resembles production. Incidentally, we could put all our API requests behind the proxy if we wanted to by using a standard prefix like /api, but for this demo we'll just proxy the one endpoint.\n\nWe have a front end app ready to consume our API. But we need to generate an OpenAPI client first.\n\nGenerate our OpenAPI client\n\nWe'll add an npm script to our package.json in the client folder to generate our OpenAPI client using openapi-ts:\n\nThis, when run, will generate a TypeScript client in src/GeneratedClient.ts based on the OpenAPI spec exposed by our .NET API. It will also include the \"root types\" so we can import them in our code easily. To generate the client, we need to ensure our API is running. So we'll jump back up to the root of our .NET / React project and we'll add a package.json. We'll add the following two dependencies:\n\nThen we'll add scripts to handle running client and server together, and to generate the client:\n\nRunning npm run generate-client in the root of our project will now:\n\n- Start the server API on http://localhost:5000\n- Wait for the OpenAPI endpoint to be available using start-server-and-test\n- Run the generate-client script in the client folder to generate the TypeScript client.\n\nHere's what our generated client looks like:\n\nYou can see our /weatherforecast endpoint is represented in the paths section and the WeatherForecast model is represented in the components.schemas section.\n\nAdjusting Microsoft.AspNetCore.OpenApi surfaced types\n\nI mentioned earlier that the temperatureC and temperatureF properties were marked as both integer and string in the OpenAPI spec. This is because Microsoft.AspNetCore.OpenApi is being ... interesting ... about number types. If we look at the types created in our client we see:\n\nNote how temperatureC and temperatureF are both number | string. This isn't what we're after; we want them to be just number to reflect the Cint model. To fix this, we can create 2 IOpenApiSchemaTransformer implementations to fix up the number | string types to just number types. One to handle integer style numbers (IntegerSchemaTransformer) and one to handle numbers with decimal places (NumberSchemaTransformer).\n\nAnd the Program.cs is updated to register these transformers:\n\nWith this in place, when we next run npm run generate-client from the root of our project, we find that our generated client now has the correct types for temperatureC and temperatureF:\n\nI've inquired whether the default behaviour makes the most sense here.\n\nConsume our generated API client\n\nNow we want to make use of our generated client in our React app. First we're going to install openapi-fetch to help with making requests:\n\n(A quick note, openapi-fetch is not strictly necessary here, but it makes things easier. It provides a fetch-based HTTP client which works well with openapi-ts generated clients. It's worth saying that there are plans to deprecate openapi-fetch which you can read about here. As of right now though, it's still a useful library to use alongside openapi-ts.)\n\nNow let's start our client and server with npm run start. We'll then replace the contents of App.tsx with:\n\nLet's break down what's happening here:\n\n- We import the generated types from GeneratedClient.ts\n- We create an openapi-fetch client using those types.\n- In a useEffect hook, we call the /weatherforecast endpoint using the generated client.\n\nFrom a users perspective, when we run the app we see: (I've reused the GIF from my previous post here as the experience is the same.)\n\nSummary\n\nIn this post we've seen how to create a .NET Web API which exposes an OpenAPI endpoint using Microsoft.AspNetCore.OpenApi. We've then seen how to generate a TypeScript client from that OpenAPI spec using openapi-ts. Finally, we've seen how to consume that generated client in a React + TypeScript application.\n\nWhat's significant here is that we have static typing all the way from back end to front end. The Cmodels we defined in our .NET API are represented in the OpenAPI spec, and those same models are represented in TypeScript types in our front end application. This means that if we change a model on the back end, we can regenerate the TypeScript client and get type safety on the front end too. I'm using C#, but you could be using something else entirely on the back end, as long as it can produce an OpenAPI spec.\n\nThere was a little adjustment needed to get the number types working correctly, but overall this was a pretty straightforward process. If you're building full stack applications with TypeScript on the front end and .NET on the back end, I recommend giving this approach a try!",
  "title": "Full-stack static typing with OpenAPI TypeScript and Microsoft.AspNetCore.OpenApi"
}