Chm.ski Labs

Bridging Rust and TypeScript with zod_gen – End-to-End Type Safety at Cimatic

Solving type drift between Rust backends and TypeScript frontends with automatic code generation. Two open-source crates that generate Zod schemas and TypeScript types directly from Rust structs.

Kamil Chmielewski 3 min read Development

Building Cimatic's MVP sent me down several technical paths—some more interesting than others. The big architectural picture deserves its own post next week (subscribe to the RSS feed to catch it), but here's a smaller piece that turned out pretty useful: unified types between Rust and TypeScript. Write a struct once in Rust, then use it safely everywhere – API, server-side rendering, front-end validation, tests, docs.

As a side-effect, I ended up building two small crates that handle this—turned out pretty useful:

Crate What it does
zod_gen Library + helpers for building Zod schemas in Rust
zod_gen_derive proc-macro implementing #[derive(ZodSchema)] so the boilerplate disappears

Together they let me infer Zod validation and TypeScript types directly from the Rust structures that already power the backend.


The Problem

You know how it goes. You start with clean Rust structs for your API:

  1. Rust structs defined the DB models and HTTP payloads.
  2. TypeScript interfaces duplicated that shape on the front-end.
  3. Zod schemas duplicated it again for runtime validation.

Three sources of truth → triple maintenance pain. Change a field in Rust? Better remember to update the TypeScript interface. Add validation? Don't forget the Zod schema. Miss one? You get those annoying type mismatches that could've been caught at build time.


The Solution in 30 Seconds

The idea was simple: make the Rust struct the source of truth for everything else.

// backend/src/models/user.rs
use zod_gen::ZodSchema;
use zod_gen_derive::ZodSchema;

#[derive(ZodSchema, serde::Serialize, serde::Deserialize)]
pub struct User {
    id: u64,
    name: String,
    email: String,
    is_admin: bool,
}

That's it. One derive macro.

Running a tiny generator binary…

cargo run -p generate_zod_types  # part of Cimatic

…spits out frontend/types/generated/user.ts

// Generated User.ts
import { z } from 'zod';

export const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string(),
  is_admin: z.boolean(),
});

export type User = z.infer<typeof UserSchema>;

No manual syncing. No drift.


Inside the Crates

I kept the design deliberately simple. Two small crates, each with a focused job:

zod_gen – the core

  • ZodSchema trait – every Rust type that can emit a schema.
  • Helpers (zod_string, zod_enum, zod_object, …) return ready-to-embed Zod snippets.
  • ZodGenerator – collect many schemas and write a single .ts bundle.

zod_gen_derive – the icing

  • Parses structs (named fields) & enums (unit variants).
  • Generates a ZodSchema impl that calls back into zod_gen helpers.
  • Fails fast on unsupported shapes so your build never lies.

Both crates are MIT licensed and <20 KB combined. I wanted something you could drop into any project without thinking twice about dependencies.


How It Worked Out

The migration went smoothly:

  1. Replace old hand-rolled TS interfaces with generated ones.
  2. Swap z.object({...}) declarations for imports from generated/*.ts.
  3. Delete 1,300 lines of duplicated type and schema definitions – 🔥.

Build time impact: +0.4 s (proc-macros are cached anyway). Runtime impact: zero. Bug impact: vanished – type inference works at both ends now.

The best part? That nagging feeling of "did I remember to update the frontend types?" just disappeared. The compiler handles it now.


Current Limitations

  • Only supports named-field structs and unit enums right now. Tuple structs/enums are on the todo list.

This covers all the cases I've needed so far. Most API types are just structs with named fields anyway. KISS!


What's Next?

Coming next: "SSRUI + Partials Architecture" where I'll show how Cimatic renders HTML on the server, streams small HTML partials for dynamic updates, and why I ditched a fat CSR build entirely. That's where the real architectural magic happens.

If you end up trying these crates out, I'd love to hear what you build with them. Always curious to see how others solve the same problems.


Useful Links

Happy hacking! 🚀

Tags: rust typescript zod type-safety open source cimatic web development code generation developer tools

Menu

Settings