Hey API: Auto-Generate a Type-Safe TypeScript SDK from OpenAPI β

πΈ
PexelsOkay, let me ask you something. How many times have you written code like this?
const getUsers = async (): Promise<User[]> => {
const res = await fetch('/api/users')
if (!res.ok) throw new Error('Failed to fetch users')
return res.json()
}And then wrote a separate User type. And then wrapped it in a React Query hook. And then added Zod validation on top. For every. Single. Endpoint.
I was doing the exact same thing and honestly β it felt like copying the same pattern over and over while the backend team kept changing things and I had to hunt down every place I wrote that type and update it manually π€.
Then I found Hey APIΒ and I have not looked back since.
What is Hey API? π€
Hey API (@hey-api/openapi-ts) is a code generation tool that reads your OpenAPI/Swagger spec and generates a fully type-safe SDK for you β complete with TypeScript types, React Query hooks, Axios client, and Zod validation schemas.
One command. Everything generated. Everything in sync with your backend.
The best part? Companies like Vercel, OpenCode, and PayPal are already using it in production. Itβs not some experimental side project β itβs battle-tested and MIT licensed.
So now you must be wondering β what exactly does it generate?
types.gen.tsβ all your TypeScript interfaces from the specsdk.gen.tsβ type-safe API functions for every endpoint@tanstack/react-query.gen.tsβ ready-to-use React Query hookszod.gen.tsβ Zod schemas for runtime validationclient.gen.tsβ the configured HTTP client
Sounds amazing, right?
Letβs Build It π
I built a full working example β you can check it out here: github.com/7hourspg/open-api-sdkΒ . Let me walk you through how to set it up yourself.
Install the package
pnpm
pnpm add @hey-api/openapi-ts -D -EThe -E flag pins the exact version. Hey API recommends this since itβs still
in active development and minor updates can introduce changes.
Add the generate script
In your package.json, add a script to trigger generation:
{
"scripts": {
"generate:sdk": "openapi-ts"
}
}Create the config file
Create openapi-ts.config.ts in your project root:
import { defineConfig } from '@hey-api/openapi-ts'
export default defineConfig({
input: './swagger.json', // your OpenAPI spec (local file or URL)
output: 'src/client', // where to put the generated files
plugins: [
'@hey-api/client-axios', // use Axios as the HTTP client
'@tanstack/react-query', // generate React Query hooks
'zod', // generate Zod schemas
],
})Thatβs it. Run pnpm generate:sdk and watch the magic happen β¨.
Setting Up the Client π§
After generation, you need to configure the base URL and headers once. Do this in your main.tsx before your app renders:
import { client } from './client/client.gen'
client.setConfig({
baseURL: 'https://your-api-domain.com',
headers: {
'Content-Type': 'application/json',
},
})Now every generated function will use this config automatically. No need to pass the base URL anywhere else.
Using the Generated Hooks β¨
This is where it gets really fun. Hereβs what consuming a GET /users endpoint looks like after generation:
import { useQuery } from '@tanstack/react-query'
import { getApiV1UsersOptions } from '../client/@tanstack/react-query.gen'
function UsersList() {
const { data, isLoading, error } = useQuery(getApiV1UsersOptions())
if (isLoading) return <p>Loading...</p>
if (error) return <p>Something went wrong</p>
return (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.userName}</li>
))}
</ul>
)
}data is fully typed here. No casting, no guessing. The types come directly from your OpenAPI spec. If the backend changes the response shape and updates the spec, you just re-run pnpm generate:sdk and TypeScript will immediately tell you everywhere something broke. π€―
What About Mutations? π
Same story. POST, PUT, DELETE β all covered.
import { useMutation } from '@tanstack/react-query'
import { postApiV1UsersMutation } from '../client/@tanstack/react-query.gen'
function CreateUser() {
const { mutate, isPending } = useMutation(postApiV1UsersMutation())
const handleSubmit = () => {
mutate({
body: { userName: 'Rajiv', password: 'secret123' },
})
}
return (
<button onClick={handleSubmit} disabled={isPending}>
{isPending ? 'Creating...' : 'Create User'}
</button>
)
}The naming convention is consistent and predictable β queries get Options as a suffix, mutations get Mutation. Once you see it once, you just know how to use every other endpoint.
The Generated Files Explained π
Let me quickly walk through what each generated file actually does:
src/client/
βββ client.gen.ts # Axios instance + setConfig
βββ types.gen.ts # All TypeScript interfaces
βββ sdk.gen.ts # Raw API functions
βββ zod.gen.ts # Zod validation schemas
βββ @tanstack/
βββ react-query.gen.ts # useQuery/useMutation wrappersYou never touch these files manually. They are generated output β treat them like node_modules. The source of truth is your swagger.json.
Never edit the files inside src/client/ directly. Any manual changes will be
overwritten the next time you run generate:sdk.
Keeping the SDK in Sync π
When your backend team updates the API:
- Get the new
swagger.jsonfrom them (or from a URL) - Run
pnpm generate:sdk - Fix the TypeScript errors that pop up
Thatβs the workflow. Step 3 is the key one β TypeScript errors are your changelogs now. No more βoh they changed the response format and now everything is undefinedβ moments at runtime.
Why This is a Big Deal β€οΈ
I want to be real with you here β the first time I ran pnpm generate:sdk and saw @tanstack/react-query.gen.ts appear with fully-typed hooks for every endpoint, I genuinely got excited. Like actually excited about a config file π.
Because think about what youβre eliminating:
- Writing type interfaces that mirror what the backend already defined
- Writing fetch wrappers for every endpoint
- Setting up React Query
queryFnandqueryKeyfor each query - Forgetting to update types when the backend changes
All of that is just gone. You focus on your UI and your business logic. The boring plumbing is handled.
Conclusion π
If youβre working with a REST API that has an OpenAPI spec, there is no reason to write your API client manually anymore. Hey API takes the spec and hands you back everything you need β types, functions, hooks, and validators β in seconds.
Try it out on your next project. Run that one command and see what appears. I promise youβll wonder how you lived without it.
Thanks for reading! β¨ Drop a comment if you have any questions or if youβre already using something similar β would love to know what you think.
Bye for now β¦
Comments
Leave a comment or reaction below!