Why our TypeScript types come from Rust

Specta generates the client types from the API's own structs. It adds a step to the workflow and removes an entire class of runtime bug.

One of the better decisions in building hiignite was letting the TypeScript types come from the Rust — not written alongside it, generated from it, by Specta.

I touched on this in the stack overview, but it deserves its own post, because it's the piece I'd keep if I had to throw the rest away.

The problem it solves: drift

In past projects, the mobile app and the web client would slowly drift out of sync with the API. Someone renames a field on the backend. The clients carry on referencing the old name. Nothing complains — TypeScript is perfectly happy, because the type it's checking against is one a person wrote by hand, not the one the API actually returns.

So you find out at runtime. Usually on a device. Usually after it shipped.

The insidious part is that hand-written client types look like type safety. You get the autocomplete, the red squiggles, the whole experience. What you don't get is any guarantee that the description still matches the thing being described.

What Specta changes

Specta generates the TypeScript straight from the Rust structs, so there's one source of truth and it's the one the API actually serialises:

#[derive(Serialize, Type)]
pub struct Client {
    pub id: Uuid,
    pub name: String,
    pub active: bool,
}

…becomes:

export type Client = { id: string; name: string; active: boolean }

The mobile app and the web dashboard both consume that generated file. Neither can drift from what the API returns, because neither holds an independent opinion about what the API returns.

Rename name to full_name in the Rust, regenerate, and both clients stop compiling — in exactly the places that reference it. The bug that used to surface as a blank field on somebody's phone now surfaces as a build error on my laptop, before I've pushed anything.

The tradeoff: no free lunch

Every time the Rust structs change, I have to rebind and regenerate. That's a real step in the workflow, and it's a step I can forget. Forget it and the generated types are stale — which puts you back in drift territory, just with a shorter fuse, because the next regeneration surfaces everything at once.

I won't pretend that's free. It's friction, it's mine to remember, and anyone who tells you code generation costs nothing hasn't run it on a Friday evening.

Still worth it, every time

Catching a type mismatch at compile time instead of in production is worth an extra step in the workflow. Every time.

The asymmetry is what makes it easy. Forgetting to regenerate costs me a confusing minute and a rebuild. Not having the generated contract costs a runtime bug on someone else's device, found by them, reported through a channel that may not exist yet, debugged without the stack trace I'd want. One of those is an inconvenience. The other is the kind of bug that erodes trust in the whole app.

For a solo build in evenings and weekends, that trade is not close.

Prefer hiignite on Google Mark hiignite as a preferred source and these posts show up more often in your Google Search results. Add as preferred source

Want to see what I'm building?

hiignite is personal trainer and online coaching software — free to start, $19/mo Pro, no per-client or per-message fees. Join the waitlist for founders-tier pricing.

// No spam. One email when we launch.