Imagine catching API integration errors before they hit production, having perfect autocompletion for your backend endpoints, and never having to manually update API types again. This isn't just a dream - it's possible with end-to-end type safety. In this post, I'll show you how I built a fully type-safe system that bridges Python and TypeScript, making full-stack development a breeze.
Why This Matters: Real-World Impact
Before we dive into the implementation, let's look at how this setup transforms your development experience:
1. Catch Errors Early
// Without type safety - Problems only appear at runtime 😱const response = await fetch("/user/123");console.log(response.data.emailAddress); // Runtime error: undefined// With our type-safe SDK ✨const user = await sdk.users.getUser(123);console.log(user.email); // TypeScript error if you try to access wrong property
2. Perfect Autocompletion
Your IDE instantly shows you:
All available API endpoints
Required parameters and their types
Response data structure
Validation rules from your Python models
3. Automatic Updates
When you modify your FastAPI models:
class User(BaseModel): id: int name: str email: str is_active: bool # Add a new field role: UserRole
Seamless team collaboration between frontend and backend
The Challenge
When building full-stack applications with different languages (Python for backend and TypeScript for frontend), maintaining type safety across the boundary can be challenging. Common issues include:
Manually keeping types in sync
Runtime type errors from API responses
Inconsistent API documentation
Time-consuming SDK maintenance
The Solution: Our Tech Stack
We'll use:
- Backend: `Python` with `FastAPI`- Frontend: TypeScript with `Next.js` or `React` or `Expo` (works with any ts/js framework)- Tools: `Turborepo`, `hey-api`, and `pnpm`
# Frontend Monorepoapps/ ├── web/ # Next.js frontend └── mobile/ # React Native Expo frontendpackages/ └── sdk/ # Generated TypeScript SDK# Separate Backend Repositoryfastapi-backend/ # FastAPI backend in its own repository
The beauty of this setup is that it doesn't matter where your FastAPI backend lives - all we need is access to its OpenAPI specification URL. This flexibility allows you to:
Keep the backend in the same monorepo for tighter integration
Maintain it in a separate repository for independent scaling
Even use an existing FastAPI service - as long as you have access to its OpenAPI spec
The SDK generation process remains the same regardless of where your FastAPI backend is hosted. This is particularly useful when:
Working with existing backend services
Different teams manage frontend and backend
You need different deployment cycles for frontend and backend
The backend needs to be used by multiple frontend applications
Backend: FastAPI with Type Definitions
In our FastAPI application, we define our models using Pydantic:
Important: After setting up the SDK configuration, you need to generate the SDK by running:
pnpm gen
This command will fetch the OpenAPI spec from your FastAPI backend and generate the type-safe SDK. Remember to run this command whenever you make changes to your API endpoints or models.
Next.js Configuration
For the types to be properly recognized in your Next.js application, you need to add the SDK package to the transpilePackages configuration in your next.config.js:
The workspace:* syntax tells pnpm to use the local workspace version of the SDK, which is essential for monorepo setups.
Now we can set up our SDK with interceptors for authentication and error handling:
// sdk/client.tsimport { acmeSDK, OpenAPI } from "@acme/sdk";// Set up request interceptor for authenticationOpenAPI.interceptors.request.use((options) => { options.headers = options.headers ?? {}; options.headers = { ...options.headers, Authorization: `Bearer ${/* your auth token */}`, }; return options;});// Set up response interceptor for error handlingOpenAPI.interceptors.response.use((response) => { if (!response || response.status >= 400) { // Handle authentication errors if (response.status === 401) { window.location.assign("/api/auth/login"); } throw new Error(`Request failed with status ${response?.status}`); } return response;});// Initialize the SDK with base URL and interceptorsconst sdk = new acmeSDK({ BASE: process.env.API_URL ?? "http://localhost:8080", interceptors: { request: OpenAPI.interceptors.request, response: OpenAPI.interceptors.response, },});export { sdk };
This setup provides:
Automatic authentication token injection
Global error handling
Environment-based API URL configuration
Type-safe request and response handling
Using the Generated SDK in Next.js
Now we can use our type-safe SDK in our Next.js application. The method names in the SDK are automatically derived from the operation IDs defined in our FastAPI backend:
import { sdk } from "@/lib/sdk";import { useState, useEffect } from "react";import type { User } from "@acme/sdk";export default function UserProfile({ userId }: { userId: number }) { const [user, setUser] = useState<User>(); useEffect(() => { // The method name 'getUser' comes from the operationId in FastAPI // @app.get("/users/{user_id}", operation_id="getUser") async function fetchUser() { try { const user = await sdk.users.getUser(userId); setUser(user); } catch (error) { console.error("Failed to fetch user:", error); } } fetchUser(); }, [userId]); return ( <div> {user && ( <> <h1>{user.name}</h1> <p>{user.email}</p> <span>{user.is_active ? "Active" : "Inactive"}</span> </> )} </div> );}
The SDK methods are organized by resource (like users, posts, etc.) and the method names are derived from the operation IDs in your FastAPI routes. For example:
# In your FastAPI backend@app.get("/users/{user_id}", operation_id="getUser")async def get_user(user_id: int) -> User: ...@app.post("/users", operation_id="createUser")async def create_user(user: UserCreate) -> User: ...# These become available in your SDK as:# sdk.users.getUser(userId)# sdk.users.createUser(userCreateData)
This naming convention ensures consistent and predictable API method names across your entire application.
Automating SDK Generation
We can automate SDK generation in our Turborepo pipeline:
This isn't just about clean code - it's about building better products faster. With this setup:
🚀 Development Speed
50% faster API integration - No more back-and-forth with API docs
90% fewer runtime errors - Catch type mismatches before they hit production
Instant refactoring - Change API structures with confidence
💡 Developer Experience
Write code with confidence, knowing TypeScript has your back
Get instant feedback when you make a mistake
Focus on building features instead of debugging type issues
🤝 Team Collaboration
Backend changes automatically propagate to frontend
Clear contracts between services
Easier onboarding for new team members
📈 Business Impact
Faster time to market
Fewer production bugs
Lower maintenance costs
Better code quality
The combination of FastAPI's automatic OpenAPI generation, hey API's SDK generation, and Turborepo's monorepo management creates a development experience that's not just type-safe, but genuinely enjoyable.
Ready to transform your development workflow? Check out the complete code on my GitHub and start building with confidence!
Have questions or want to share your experience? Feel free to reach out on Twitter or LinkedIn - I'd love to hear how you're using these tools in your projects.