This is the part 2 of the "API first" article series about TypeSpec, OpenAPI, React/Tanstack and Microsoft Dynamics 365 Business Central. Please ensure you have read part 1 before you go on with this article.

Setting up the frontend application

To use the Sales Order TypeSpec and OpenAPI definition, we need to set up our frontend application. A good starting point here is to use Tanstack Query with openapi-client-axios. Let's dive in. In your root of <project-name> folder, let's run:

npm create @tanstack/start@latest

Instead of setting up the folder structure manually, fork and clone the complete project here: github.com/sn4h/vite-tanstack-typespec-openapi

If you haven't used TanStack before, this is your chance! It's a suite of headless, type-safe libraries for managing asynchronous state.

Installing dependencies

Next, let's install the openapi-client-axios and the openapicmd:

npm install --save axios openapi-client-axios
npm install --save-dev openapicmd

To our package we now need to specify a command to build the types based on the openapi.yaml:

"api:build": "npm run tsp:compile && npx openapi typegen ./public/definition/openapi.yaml > src/@types/openapi.d.ts"

Configuring Axios for type safety

Next, to make Axios type safe we need to define the OpenAPIClientAxios and pass in the openapi definition:

import { OpenAPIClientAxios } from 'openapi-client-axios'
import type { Client } from '@/@types/openapi'

const api = new OpenAPIClientAxios({
  definition: '/definition/openapi.yaml',
})

let clientInstance: Client | null = null

export const getAxiosClient = async (): Promise<Client> => {
  if (!clientInstance) {
    clientInstance = await api.getClient<Client>()
  }
  return clientInstance
}

Using the client in routes

And in our index route in the routes folder we can now use this client to query our SalesOrders:

import { createFileRoute } from '@tanstack/react-router'
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
import { getAxiosClient } from '@/lib/axios-client.ts'
import { SalesOrderResponse } from '@/@types/openapi'

export const SALES_ORDER_QUERY_KEY = 'salesOrder'

export const fetchSalesOrders = async () => {
  const client = await getAxiosClient()
  const response = await client.SalesOrder()

  return response.data as SalesOrderResponse
}

const salesOrderQueryOptions = queryOptions({
  queryKey: [SALES_ORDER_QUERY_KEY],
  queryFn: () => fetchSalesOrders(),
})

export const Route = createFileRoute('/')({
  loader: ({ context: { queryClient } }) =>
    queryClient.ensureQueryData(salesOrderQueryOptions),
  component: App,
})

function App() {
  const { data } = useSuspenseQuery(salesOrderQueryOptions)

  return (
    <div className="p-4">
      <header>Hello, SalesOrders!</header>
      <article>
        <ul>
          {data.value.map((order) => (
            <li key={order.id}>{order.number}</li>
          ))}
        </ul>
      </article>
    </div>
  )
}

Conclusion

That's it, end-to-end type safety with TypeSpec, OpenAPI, Axios and Tanstack.

Full source of this example is found here:

Thanks for reading