If you’ve ever built a frontend and backend at the same time, you know the pain.
Did that endpoint change? Is it /v1/customers
or /api/customers
this time?
And wait,was that field name
, full_name
, or display_name
?
We’ve all been there. Guessing, checking docs, syncing Slack messages, and hoping your TypeScript types match what your API actually returns. It’s the modern version of “it works on my machine.”
When we started building our product, I promised myself: no more guessing games.
We wanted the backend and frontend to speak the same language, literally.
But there was one catch: we weren’t using tRPC. Our backend was written in FastAPI, not TypeScript.
Still, we refused to give up on type safety. Here’s how we built a stack where Python and TypeScript stay perfectly in sync, no manual typings, no runtime surprises.
FastAPI was our backend of choice because it forces clarity. Every endpoint is defined with Python type hints and Pydantic models. That means your validation, docs, and OpenAPI schema are all generated for free.
Out of the box, FastAPI gives you a live spec at /openapi.json
.
That’s not just a nice dev tool, it’s an always-up-to-date contract for your entire system.
During local development, we expose it at http://localhost:8000/openapi.json
so the frontend can pull from it directly. The backend defines reality; the frontend just follows.
openapi-ts
Our philosophy: the backend defines the truth, the frontend imports it.
openapi-ts
makes that possible. With one command, it converts your FastAPI OpenAPI schema into TypeScript definitions.
Here’s how easy it is to set up:
1. Install the package
bun add openapi-typescript
2. Add a script in your frontend’s package.json
"scripts": {
"generate-types": "openapi-typescript http://localhost:8000/openapi.json -o src/types/api-schema.ts"
}
Now, running bun generate-types
regenerates your API types automatically:
bun run generate-types
You’ll get a file like src/types/api-schema.ts
, containing every route, request, and response ready to import anywhere in your app.
Example:
import type { components } from "./api-schema";
export type User = components["schemas"]["UserSchema"];
No manual maintenance. No mismatched shapes. Just perfectly aligned types.
openapi-fetch
Generating types is only half the story, you also need a way to use them.
We didn’t want yet another fetch wrapper. We wanted something that actually knew what endpoints existed, what data they expected, and what they returned.
openapi-fetch
does exactly that.
It’s part of the same ecosystem, designed to work seamlessly with openapi-ts
.
Here’s the setup:
import createClient from "openapi-fetch";
import { getConfig } from "@/config";
import type { paths } from "@/types/api-schema"; // generated types
const config = getConfig();
export const apiClient = createClient<paths>({
baseUrl: config.apiUrl,
credentials: "include",
});
The result? Instant autocomplete. Compile-time validation.
Change a backend parameter or remove a field, and your frontend won’t even compile.
That’s the kind of failure we like: the safe kind.
Here’s our new workflow in action:
Define a route in FastAPI. Add your path, input, and output models.
Run the backend. FastAPI automatically updates /openapi.json
.
Regenerate frontend types.
bun run generate-types
Use it with openapi-fetch
. You get fully typed requests right in your editor.
No manual syncs. No shared constants file. No Slack pings asking,
“Hey, did you rename that field again?”
It’s clean, reliable, and scales beautifully as your API grows.
Here’s the full picture:
Together, they form a self-documenting, type-safe feedback loop between backend and frontend.
It’s not magic, it just feels like it.
You don’t need tRPC to get a type-safe workflow.
If your backend already speaks OpenAPI (and FastAPI does), you’re just a few commands away from full-stack type safety.
Because the best kind of integration?
The one that never drifts out of sync.