ARTICLE AD BOX
It sounds like you are hitting the classic "Double Declaration" fatigue. That "unnatural" feeling comes from manually writing runtime schemas (Zod) that essentially duplicate your static types (TypeScript interfaces).
You mentioned building a custom solution to serve types via HTTPS. You essentially re-invented a pattern that already exists, but the friction remains because you are still manually maintaining the contract.
Here is how to solve this using standardized tools without hitting a dead end:
1. The "Code-First" Approach If you want the "natural" feeling of just writing TypeScript classes/interfaces and having everything else happen automatically, look at tsoa. Instead of writing Zod schemas, you write standard controllers and interfaces. The library uses AST analysis at build time to auto-generate:
The OpenAPI spec (swagger.json) for your clients.
The Express/Node routes with automatic runtime validation.
This removes the need to write Zod manually. Your TS interface is the contract.
2. If you want to keep Zod: Automate it If you prefer Zod but hate the manual labor, you shouldn't be writing the schemas by hand. Use ts-to-zod.
Write your single source of truth in TypeScript (interface User { ... }).
Run the watcher.
It generates the Zod schemas for you (const UserSchema = ...).
3. Regarding your "Serving Types" solution The solution you built (serving types from backend to frontend for build time) is basically a custom implementation of Federated Types (part of Module Federation). If you want to standardize that part of your architecture, look into @module-federation/typescript. It handles the "downloading types from a remote host" part correctly, but you will still need one of the solutions above (tsoa or codegen) to handle the runtime validation logic inside the API.
Recommendation: Since you mentioned DX is the priority: drop the manual Zod definition. Switch to a solution that derives the contract from your code (like tsoa) or generates the client SDK directly from your backend types.
