基于TypeScript的类型安全客户端-服务端API实现方案问询
Great question! This is a common pain point when building TypeScript full-stack apps—redundant type definitions, mismatched request/response types, and forgotten endpoint registrations can lead to bugs and maintenance headaches. Let's walk through a TypeScript-native solution to enforce contract consistency, then cover code generation tools that can automate this workflow.
1. Type-Native Contract Binding (No Code Gen Needed)
We can use TypeScript's advanced type system to create a single source of truth for API contracts, then derive client/server functions and enforce endpoint registration automatically.
Step 1: Define a Unified API Contract
First, create a shared file that ties together endpoints, request arguments, and response types for all your APIs:
// ./api.ts // Base type for all API commands type ApiCommand = { endpoint: string; arg: unknown; resp: unknown; }; // Individual API contracts type GetUser = { endpoint: '/users'; arg: { id: string }; resp: { name: string }; }; type CreateUser = { endpoint: '/users/create'; arg: { name: string }; resp: { id: string; name: string }; }; // Union of all API commands (enforces completeness later) type AllApiCommands = GetUser | CreateUser; // Derive client function type: auto-infers endpoint, args, and return type type ClientFn<Cmd extends ApiCommand> = (http: HttpClient, arg: Cmd['arg']) => Promise<Cmd['resp']>; // Derive server handler type: auto-infers args and return type type ServerHandler<Cmd extends ApiCommand> = (db: Db, arg: Cmd['arg']) => Promise<Cmd['resp']>; // Force server to implement handlers for ALL endpoints type ServerHandlerRegistry = { [Cmd in AllApiCommands as Cmd['endpoint']]: ServerHandler<Cmd>; };
Step 2: Type-Safe Client Implementation
The client can use the contract to create strongly-typed functions without repeating type definitions:
// ./client-api.ts import { AllApiCommands, ClientFn, GetUser, CreateUser } from './api.ts'; // Optional helper to reduce boilerplate const createClientApi = <Cmd extends AllApiCommands>(endpoint: Cmd['endpoint']) => { return (http: HttpClient, arg: Cmd['arg']) => http.get<Cmd['resp']>(endpoint, arg) as Promise<Cmd['resp']>; }; // Strongly-typed client functions const getUser: ClientFn<GetUser> = createClientApi('/users'); const createUser: ClientFn<CreateUser> = createClientApi('/users/create'); // Usage: TypeScript enforces correct args and infers return type const user = await getUser(http, { id: '1' }); // user is typed as { name: string } const newUser = await createUser(http, { name: 'Alice' }); // newUser is typed as { id: string; name: string }
Step 3: Enforced Server Endpoint Registration
The ServerHandlerRegistry type will throw a TypeScript error if you miss any endpoint or mismatch types, and we can batch-register handlers to avoid manual work:
// ./server-api.ts import { AllApiCommands, ServerHandlerRegistry } from './api.ts'; // TypeScript forces you to implement ALL endpoints correctly const apiHandlers: ServerHandlerRegistry = { '/users': async (db, arg) => { // arg is automatically typed as { id: string } const user = await db.findById(arg.id); return { name: user.name }; // Must return { name: string } }, '/users/create': async (db, arg) => { // arg is automatically typed as { name: string } const newUser = await db.create(arg); return { id: newUser.id, name: newUser.name }; // Must return { id: string; name: string } } }; // Batch-register all endpoints in one go Object.entries(apiHandlers).forEach(([endpoint, handler]) => { httpServer.on(endpoint, async (params) => handler(db, params)); });
Key Benefits:
- No redundant type definitions—all types flow from a single contract
- TypeScript enforces matching request/response types across client and server
- Impossible to forget endpoint registration (TypeScript will flag missing handlers)
2. Code Generation Tools for Larger APIs
If you're working with a large number of endpoints, manual contract maintenance can still be tedious. Here are tools that automate this workflow:
tRPC
tRPC is the gold standard for type-safe full-stack TypeScript APIs. It lets you define your API routes on the server, and the client gets auto-generated strongly-typed functions with zero manual contract work.
Server Example:
import { initTRPC } from '@trpc/server'; import { z } from 'zod'; // For runtime validation (optional but recommended) const t = initTRPC.create(); export const appRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.string() })) // Runtime + type validation .query(async ({ input }) => { return db.findById(input.id); }), createUser: t.procedure .input(z.object({ name: z.string() })) .mutation(async ({ input }) => { return db.create(input); }) }); // Export type definition for client export type AppRouter = typeof appRouter;
Client Example:
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const trpc = createTRPCProxyClient<AppRouter>({ links: [httpBatchLink({ url: '/api' })], }); // Fully type-safe calls with auto-completion const user = await trpc.getUser.query({ id: '1' }); const newUser = await trpc.createUser.mutate({ name: 'Bob' });
Zod + Custom Code Generation
If you prefer a more flexible setup, use Zod to define request/response schemas, then use tools like zod-to-ts or custom scripts to generate client/server type definitions and function templates. Zod provides both runtime validation and TypeScript types, so you get type safety and runtime error checking.
OpenAPI + openapi-typescript
If your API needs to follow OpenAPI standards, use openapi-typescript to generate TypeScript types from your OpenAPI spec. You can then build client/server implementations around these auto-generated types to ensure full contract compliance.
内容的提问来源于stack exchange,提问作者Alex Craft




