Hello tygor
Type-Safe RPC from Go to TypeScript
TL;DR: Write Go functions, call them from TypeScript with full type safety. No schema files. Validation tags on Go structs generate matching Zod schemas. Change a field, regenerate, the compiler tells you what broke.
I've built the same mini-framework a dozen times. Wrap a Go function in something that speaks JSON on an http.Handler. On the browser side, I write a fetch wrapper and usually just hard-code the URL paths. I don't want to write schema files, just Go functions that TypeScript knows how to call.
So I built tygor. It's early. It's changing a lot. I hope you'll try it. There's as much type-safety as I could reasonably manage.
TypeScript is wonderful, and the npm ecosystem is strong. I'm a fan of what folks are building: TanStack, Nuxt, Astro, Vite, Bun. But I trust myself to secure a Go backend. I don't really trust myself to secure a JS backend. Case in point, yesterday I and many others rushed to update and deploy our Next.js apps because of a severe remote execution security issue in React Server Components1.
What It Looks Like
Here's what it looks like. You write a handler:
type GetMessageRequest struct {
ID string `json:"id"`
}
func GetMessage(ctx context.Context, req GetMessageRequest) (*Message, error) {
// ...
}
Register it:
func SetupApp() *tygor.App {
app := tygor.NewApp()
svc := app.Service("Message")
svc.Register("Set", tygor.Exec(SetMessage)) // Exec -> POST
svc.Register("Get", tygor.Query(GetMessage)) // Query -> GET (cacheable)
return app
}
func main() {
// ...
if err := http.ListenAndServe(addr, SetupApp().Handler()); err != nil {
log.Fatal(err)
}
}
Run tygor gen. Now in TypeScript:
import { createClient, ServerError } from "@tygor/client";
import { registry } from "./rpc/manifest";
const client = createClient(registry, { baseUrl: "/" });
const message = await client.Message.Get({ id: "rawr" });
That's it. The client knows the request shape, the response shape, and which HTTP method to use. Change the Go struct, regenerate, and the compiler tells you what broke. Use the Vite plugin or any file watcher to re-generate. There's a tygor check command for CI to ensure you're in sync.
The Generator
The generator precisely follows encoding/json semantics. Everything is mapped: struct tags, omitempty, embedded fields, anonymous structs. The TypeScript types match what your JSON actually looks like.
Tygor manages your routes, handles serialization, and uses the right HTTP methods. The framework itself is quite small and should feel familiar to any Go developer who works with net/http. Most of the complexity and value in tygor is in the type generator and tooling.
tygor gen outputs TypeScript types, zod schemas (or zod-mini), and a Discovery doc (a JSON specification of your routes and types). The generator architecture supports multiple output plugins. Configure it with flags or Go code.
There are existing solutions in the Go/TypeScript type generation landscape: tygo2 is a fantastic tool, and guts3 is another impressive project. But shared struct types alone still leave you wiring up the calls. I want the full tRPC experience for Go.
Inspired by tRPC, TanStack, and Elysia, I built a Vite plugin for local dev. It watches Go files, rebuilds, regenerates types, and hot reloads. Build errors show in the browser. If the build fails or the new server won't start, the existing server keeps running. Your frontend state survives server restarts.
Streaming
Looking beyond unary RPC, I've been working on streaming primitives for tygor. Atom gives you observable single values. Stream gives you server-sent events (SSE). Same type safety, same client.
These are fairly experimental but open up simple, powerful ways to synchronize state. They're designed to work well with React, Svelte, SolidJS, Vue, etc. I hope to generate helper libraries for TanStack Query and each framework.
Streams implement AsyncIterable, so you can consume them directly:
const stream = client.Activity.Events({ userId: "123" });
for await (const event of stream) {
console.log(event.type, event.payload);
}
For reactive frameworks, use the subscribe/getSnapshot pattern. I'm really liking how tygor works with SolidJS so far:
const atom = useAtom(client.Message.State);
<div>{atom().status}</div> // connection status
<Show when={atom().data}>
{(data) => (
<div>
<div>{data().message}</div>
</div>
)}
</Show>
// Atom holding message state - subscribers get current value and updates
var messageAtom = tygor.NewAtom(&MessageState{
Message: "hello",
})
// Register with: svc.Register("State", messageAtom.Handler())
Validation
Validation tags drive Go-side validation and generate Zod4 schemas. Add validate:"required,email" to a field and the generator produces a matching schema.
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=13"`
}
// Generated
z.object({
email: z.string().email(),
age: z.number().int().gte(13),
})
Configure the generated output, including custom type mapping (for example, if you have types with custom MarshalJSON methods).
func GenConfig(g *tygorgen.Generator) *tygorgen.Generator {
return g.
EnumStyle("union").
OptionalType("undefined").
WithDiscovery().
WithFlavor(tygorgen.FlavorZod)
}
The client may validate requests before they leave, responses when they arrive, or both. It supports Standard Schema5, and I'm looking at generating other client validators like ArkType.
import { registry } from "./rpc/manifest";
import { schemaMap } from "./rpc/schemas.map.zod";
const client = createClient(registry, {
schemas: schemaMap,
validate: { request: true },
});
What's Next
Next up: dogfooding with real applications, building out documentation and examples, and refining the API.
On the feature roadmap: bidirectional streaming via WebSockets, and a big focus on in-browser devtools. Inspecting RPCs and data flows without adding special instrumentation is a dream.
The project is moving fast and the API is still changing. Please try it and give me feedback. Check out the GitHub.
Footnotes
-
CVE-2025-55182 - Critical RCE vulnerability in React Server Components, CVSS 10.0 ↩
-
Standard Schema - A common interface for TypeScript validation libraries ↩