Type-safe TanStack Query with OpenAPI
TanStack Query (formerly React Query) is an asynchronous state management library for React. You tell it where to get your data - typically from an API, but it could be from any other source - and it takes care of caching, revalidation, background updates, etc.
OpenAPI (not the AI company), is an specification for documenting REST APIs. Used by big companies such as GitHub, Stripe, and Discord, it’s the most popular standard for describing APIs in a machine-readable format.
For a while, I’ve explored ways how to combine these two things together. I wanted to achieve a tRPC-like experience without having to refactor my backend.
In tRPC, you change your Prisma or Drizzle (or any type-safe ORM) schema and the types just flow to the frontend. End-to-end.
Here we are doing things slightly differently. We will leave the backend untouched. The OpenAPI specification is the source of truth.
I’m calling it… middle-to-end type safety.
The plan
We’re going to build a set of custom, type-safe, TanStack Query hooks for React that consume an API. The types are all going to be inferred from an OpenAPI specification.
In other words, we will wrap TanStack Query’s useQuery
and useMutation
hooks and sprinkling some TypeScript magic on top.
For turning OpenAPI schemas into TypeScript types, we are going to use the openapi-typescript package. We will also use the openapi-fetch, which nicely integrates with the former.
Step 1: Generating types from an OpenAPI spec
Let’s start by generating all TypeScript types from an existing OpenAPI specification.
Install the openapi-typescript
package:
For even better type-safety, turning on noUncheckedIndexedAccess
in your tsconfig.json
is recommended:
Now, let’s generate the types:
If you want to follow along with this post, I’m gonna be using GitHub’s OpenAPI specification. Here’s how you can generate types for it:
ℹ️ Keeping your API types in sync
In this post, we are fetching and generating the types from an OpenAPI specification once.
In the real-world, OpenAPI specifications change often. You might want to automate this process to avoid mismatches between the API and your client.
This doesn’t have to be complicated. A simple script with setInterval
can do the job. Here’s one that can be used as a starting point:
With packages like concurrently
, you can run this script together with your development server.
Step 2: Setup the API client
Now that we have the types, let’s set up the API client. We are going to use openapi-fetch
for this.
Install the openapi-fetch
package:
Create a new file called client.ts
:
In this demo we are going to fetch Gists from GitHub, so we will need an API token to make requests.
In openapi-fetch
, you can also define a middleware function that runs before each request:
At this point, we have a type-safe API client that can make authorized requests to the GitHub API.
Step 3: Create custom TanStack Query hooks
Now that we have a fully typed API client, let’s create a set of custom TanStack Query hooks using it.
We’re gonna thinly wrap TanStack Query’s useQuery
and useMutation
hooks.
The goal is to replicate the same type experience as we have with openapi-fetch
, this time with TanStack Query’s hooks.
The final API will be a combination of queries and mutations with HTTP methods.
For example, to call a POST
endpoint:
So let’s begin.
POST hook
Create a new file called hooks.ts
.
We will start with a naive implementation of the useMutation
hook, with no type safety:
Now let’s add types. We will need the help of an utility package called openapi-typescript-helpers
:
We want the path
parameter to only be inferred from the paths that have a POST
method in the OpenAPI specification.
And then, from the path, we want to infer the parameters (body, params, querystring) and return type (response) of that exact endpoint.
Below, I’m defining two utility types for that: Paths
and Params
. I’ve also updated the usePostMutation
hook to extend from them:
We’re almost there. The missing piece is figuring out a way to pass extra options (TanStack Query options) to the useMutation
hook.
TanStack Query’s UseMutationOptions
type is a little tricky to use directly in this case.
Here I am creating a new type that only picks the options we need:
Now we can update the usePostMutation
function to accept these options:
GET hook
Now let’s create a similar hook for GET
requests.
GET
requests are typically used for reading that and are OK to be cached, so we are going to wrap the useQuery
hook this time:
We are now creating a new type, UseQueryOptions
.
This follows the same logic as the UseMutationOptions
type, but this time it’s for the useQuery
hook.
In the params
parameter, we are also adding a new property called rq
(from React Query) for passing extra options to the useQuery
hook.
The queryKey
, which controls things like caching and revalidation, receives the path (unique across all GET endpoints) and the request parameters.
Other hooks (PUT, DELETE, etc.)
PUT, and DELETE endpoints typically mutate data, so they can be implemented in a similar way to the POST hook.
Here’s how you can implement the usePutMutation
and useDeleteMutation
hooks:
At this point, we have a set of custom hooks that are fully typed according to GitHub’s OpenAPI specification. Let’s see it in action:
Step 4: Usage in a React component
Finally, let’s see how we can use these hooks in a React component.
Here’s a simple example that fetches, creates, and deletes Gists from GitHub.
You can see the useGetQuery
, usePostMutation
, and useDeleteMutation
hooks in action:
Honorable mentions
Hopefully this post has shown you that integrating TanStack Query and OpenAPI together is not that hard.
If that wasn’t the case, there other open source tools that can help you with this task:
Each of these brings their own set of features and trade-offs, so make sure to check them out.
Conclusion
In this post, we learned how to turn an OpenAPI specification into type-safe TanStack Query hooks.
By using openapi-typescript
and openapi-fetch
, we were able to generate TypeScript types from an OpenAPI specification and create a fully typed API client.
We then created a set of custom hooks that wrap TanStack Query’s useQuery
and useMutation
hooks.
The end result is a set of hooks that are fully typed according to the OpenAPI specification.
All the code from this post is available in this repository. Feel free to check it out.
Questions or feedback? Feel free to reach out to me on X / Twitter. ✌️