Generate a TypeScript HTTP Client From An OpenAPI Spec In DotNET 5

Posted on: Sunday, 17 October 2021

I've recently been doing a bit of .NET development (which I love btw) and thought it might be useful for others to see how I generate a TypeScript HTTP client from a .NET Web API using OpenAPI code generators.

But first to explain why we want to do this, and there's mainly two reasons for doing so:

  • An auto-generated HTTP client provides a great DX for consumers. Instead of faffing about with request URLs, headers, body etc etc, you simply consume a SDK and construct API instances and call methods on those instances.
  • Using TypeScript means our data layer is fully typed. This means the data objects you work with client side are EXACTLY the same type as the DTO objects returned from the API. This again makes for a better DX on the front-end as well as reduces runtime bugs.

There are many ways to provided fully typed network/data layers, for exampling GraphQL or gRPC, but if you're working with traditional REST then the easiest way to generate types is to utilise OpenAPI.

A standard .NET 5 WebAPI project (dotnet new webapi) comes with OpenAPI already integrated, meaning the webapi will generate a valid OpenAPI Specification of your API. You could then just point your OpenAPI generator at the swagger endpoint, but this means running the project, which is not really something we want to do in build pipelines.

Instead we want to extract the OpenAPI from the built .dll. To achieve this we need to install the Swashbuckle CLI tool: dotnet tool install Swashbuckle.AspNetCore.Cli.

Now we can extract the OpenAPI spec:

dotnet build
dotnet swagger tofile --output api-spec.json ./bin/Release/net5.0/WebAPI.dll v1

You then generate the TypeScript/Fetch HTTP client using the OpenAPI generator like so:

$(npm bin)/openapi-generator-cli generate -i ./api-spec.json -g typescript-fetch -o ./HttpClient/ --additional-properties=typescriptThreePlus=true

Consuming the Client

Construct the ApiClient:

import {
} from './HttpClient';

const configParams: ConfigurationParameters = {
  basePath: 'https://localhost:5001',
  middleware: [],

const apiConfig = new Configuration(configParams);

export const apiClient = {
  weatherForecastApi: new WeatherForecastApi(apiConfig),

export type ApiClient = typeof apiClient;


The client supports middleware which can be useful for passing auth tokens:

import {
} from './HttpClient';

export class ApiMiddleware implements Middleware {
  public async pre(context: ResponseContext): Promise<FetchParams | void> {
    const accessToken = this.acquireToken();
    return {
      url: context.url,
      init: {
        headers: new Headers({
          Authorization: `Bearer ${accessToken}`,

  public post(context: ResponseContext): Promise<Response | void> {
    return Promise.resolve(context.response);

  private acquireToken(): Promise<string> {
    return Promise.resolve().then(() => {
      return 'ACCESS_TOKEN';

You pass in the middleware with the config:

const configParams: ConfigurationParameters = {
  basePath: 'https://localhost:5001',
  middleware: [new ApiMiddleware()],

And there we go, you have a fully typed HTTP client you can use in your client.

Calling Endpoints

apiClient.weatherForecastApi.weatherForecastGet({ signal })

In the above example we pass in the fetch initOptions which includes a custom signal, which is used for request cancellation (more on that below).

Usage With React

I use react-query to call my api methods, as it provides a lot of additional features like response caching & cancellation.

I create query hooks for each endpoint:

import { WeatherForecast } from './HttpClient';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ResponseError } from '../types';
import { withQueryCancellation } from '../util/withQueryCancellation';
import { useApiClient } from './useApiClient';

export const getWeatherForecastKey = 'getWeatherForecast';

export function useGetWeatherForecast(
  options?: UseQueryOptions<Array<WeatherForecast>, ResponseError>
): UseQueryResult<Array<WeatherForecast>, ResponseError> {
  const { weatherForecastApi } = useApiClient();
  return useQuery(
    withQueryCancellation((signal) =>
      weatherForecastApi.weatherForecastGet({ signal })

Thanks to a change I made to the generator, it now supports request cancellation. Here's the implementation of withQueryCancellation:

import { QueryFunction } from 'react-query';

export function withQueryCancellation<T = unknown>(
  cb: (signal: AbortSignal, ...args: unknown[]) => Promise<T>
): QueryFunction<T> {
  return (...args: any) => {
    const controller = new AbortController();
    const signal = controller.signal;
    const promise = cb(signal, ...args);
    // @ts-expect-error
    promise.cancel = () => controller.abort();
    return promise;

You then consume the query hooks in your react components, for example:

export const App: React.FC = () => {
  const {
    data = [],
  } = useGetWeatherForecast();

  return (
    <div className="App">
      {isFetching && <p>Loading...</p>}
      {error && <p>{getResponseErrorMessage(error)}</p>}
      {, i) => (
        <p key={i}>
          {forecast.summary} ({forecast.temperatureC}C)

How nice is that? Remember data returned from useGetWeatherForecast is typed, and you can use the type interfaces in your component prop definitions.

Types react-query hooks

In the above screenshot you can see data is of type WeatherForecast[].

Example Project

Here's an example project with everything setup:

If you have any questions please leave a comment below!


(No comments)

Add a new comment