Shared Types & API Client
The biggest advantage of full-stack TypeScript is sharing types between frontend and backend. When the API response type changes, both sides get type errors immediately — no more runtime surprises from mismatched data shapes.
40 min•By Priygop Team•Last updated: Feb 2026
Full-Stack Type Architecture
- Shared types package — Create a shared/ directory with types used by both frontend and backend
- API contracts — Define request/response types in the shared package. Both sides import the same types
- Type-safe API client — A fetch wrapper that returns typed responses: fetchApi<User>('/api/users/1')
- Zod schemas — Define validation schemas that generate TypeScript types. Runtime validation + compile-time safety
- tRPC — End-to-end type safety. Client function calls map directly to server functions with full type inference
- OpenAPI/Swagger — Generate TypeScript clients from API specifications. Auto-sync types with backend changes
Type-Safe API Client Code
Example
// shared/types.ts — Shared between frontend and backend
export interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: string;
}
export interface CreateUserDto {
name: string;
email: string;
role: User["role"];
}
export interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
total: number;
page: number;
limit: number;
}
// frontend/api.ts — Type-safe API client
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private async request<T>(
endpoint: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
const res = await fetch(`${this.baseUrl}${endpoint}`, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!res.ok) {
throw new Error(`API Error: ${res.status}`);
}
return res.json();
}
// Typed methods
async getUsers(): Promise<PaginatedResponse<User>> {
return this.request("/users");
}
async getUser(id: number): Promise<ApiResponse<User>> {
return this.request(`/users/${id}`);
}
async createUser(data: CreateUserDto): Promise<ApiResponse<User>> {
return this.request("/users", {
method: "POST",
body: JSON.stringify(data),
});
}
}
const api = new ApiClient("https://api.example.com");
const { data: user } = await api.getUser(1);
// TypeScript knows: user is User type