tygor

package module
v0.7.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 30, 2025 License: MIT Imports: 15 Imported by: 7

README

tygor banner Go Reference NPM

tygor

Type-safe Go backend for web apps.

Write Go functions, call them from TypeScript with full type safety. No IDL required.

What it looks like

Write Go types and handlers:

type User struct {
	ID    int64  `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email" validate:"required,email"`
}

type GetUserRequest struct {
	ID int64 `json:"id"`
}

type CreateUserRequest struct {
	Name  string `json:"name" validate:"required,min=2"`
	Email string `json:"email" validate:"required,email"`
}

func GetUser(ctx context.Context, req *GetUserRequest) (*User, error) {
	// ...
}

func CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) {
	// ...
}

tygor generates TypeScript types:

// Code generated by tygor. DO NOT EDIT.
export interface User {
  id: number;
  name: string;
  email: string;
}

And Zod schemas from your validate tags:

// Code generated by tygor. DO NOT EDIT.
import { z } from "zod";

export const UserSchema = z.object({
  id: z.number().int(),
  name: z.string(),
  email: z.string().min(1).email(), // validate:"required,email"
});

Call your API with full type safety:

const user = await client.Users.Get({ id: "123" });
// user: User (autocomplete works)

Why tygor?

  • No IDL required. Go structs are your schema. Or use protobuf if you prefer schema-first.
  • Standard HTTP/JSON. Debuggable with curl. Cacheable. Works with your existing infra.
  • Tiny client. Proxy-based, <3KB. No per-endpoint generated code.
  • Go-native. Works with net/http, your middleware, your patterns.

Who is this for?

Use tygor if you:

  • Build fullstack apps with Go backend + TypeScript frontend
  • Work in a monorepo (or want types to stay in sync)
  • Prefer iterating on code over maintaining schema files
  • Would use tRPC if your backend was TypeScript

Consider alternatives if you:

  • Need multi-language clients today → OpenAPI generators
  • Need strict public API contracts → Connect/gRPC with protobuf

Comparison

tygor tRPC Connect gRPC-web
Backend language Go TypeScript Any Any
Schema Optional No Protobuf Protobuf
TypeScript types Generated Inferred Generated Generated
Transport HTTP/JSON HTTP/JSON HTTP/JSON + gRPC gRPC

tygor is for Go teams who want tRPC-style ergonomics.

Installation

# Go
go get github.com/broady/tygor

# TypeScript
npm install @tygor/client

Quick Start

1. Register handlers
app := tygor.NewApp()

users := app.Service("Users")
users.Register("Get", tygor.Query(GetUser))      // GET request
users.Register("Create", tygor.Exec(CreateUser)) // POST request

http.ListenAndServe(":8080", app.Handler())
2. Generate TypeScript
tygorgen.FromApp(app).ToDir("./client/src/rpc")
3. Call from TypeScript
import { createClient } from "@tygor/client";
import { registry } from "./rpc/manifest";
const client = createClient(registry, {
  baseUrl: "http://localhost:8080",
});

const user = await client.Users.Get({ id: "123" });

Try it

Example Description
react React + Vite starter with type-safe API calls
zod Client-side validation from Go validate tags
newsserver Simple CRUD API to explore the basics

See all examples including auth, protobuf, and more.

Features

Validation

Server-side validation with validator/v10, client-side with generated Zod schemas:

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required,min=2"`
    Email string `json:"email" validate:"required,email"`
}

See examples/zod for client-side validation.

Caching

Cache control for GET endpoints:

users.Register("Get", tygor.Query(GetUser).
    CacheControl(tygor.CacheConfig{MaxAge: 5 * time.Minute, Public: true}))
Interceptors

Cross-cutting concerns at app, service, or handler level:

app.WithUnaryInterceptor(loggingInterceptor)
service.WithUnaryInterceptor(authInterceptor)
handler.WithUnaryInterceptor(auditInterceptor)
Error Handling

Structured error codes that map to HTTP status:

return nil, tygor.NewError(tygor.CodeNotFound, "user not found")
// → HTTP 404, JSON: {"code": "not_found", "message": "user not found"}
Middleware

Standard HTTP middleware:

app.WithMiddleware(middleware.CORS(middleware.CORSAllowAll))

Status

[!IMPORTANT] tygor is pre-release. The API and protocol may change. Pin @tygor/client and github.com/broady/tygor to the same version.

License

MIT

Tiger image by Yan Liu, licensed under CC-BY.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type App added in v0.6.1

type App struct {
	// contains filtered or unexported fields
}

App is the central router for API handlers. It manages route registration, middleware, interceptors, and error handling. Use Handler() to get an http.Handler for use with http.ListenAndServe.

func NewApp added in v0.6.1

func NewApp() *App

func (*App) Handler added in v0.6.1

func (a *App) Handler() http.Handler

Handler returns an http.Handler for use with http.ListenAndServe or other HTTP servers. The returned handler includes all configured middleware.

Example:

app := tygor.NewApp().WithMiddleware(cors)
http.ListenAndServe(":8080", app.Handler())

func (*App) Routes added in v0.6.1

func (a *App) Routes() internal.RouteMap

Routes returns route metadata for code generation. The return type is internal; this method is for use by tygorgen only.

func (*App) Service added in v0.6.1

func (a *App) Service(name string) *Service

Service returns a Service namespace.

func (*App) WithErrorTransformer added in v0.6.1

func (a *App) WithErrorTransformer(fn ErrorTransformer) *App

WithErrorTransformer adds a custom error transformer. It returns the app for chaining.

func (*App) WithLogger added in v0.6.1

func (a *App) WithLogger(logger *slog.Logger) *App

WithLogger sets a custom logger for the app. If not set, slog.Default() will be used.

func (*App) WithMaskInternalErrors added in v0.6.1

func (a *App) WithMaskInternalErrors() *App

WithMaskInternalErrors enables masking of internal error messages. This is useful in production to avoid leaking sensitive information. The original error is still available to interceptors and logging.

func (*App) WithMaxRequestBodySize added in v0.6.1

func (a *App) WithMaxRequestBodySize(size uint64) *App

WithMaxRequestBodySize sets the default maximum request body size for all handlers. Individual handlers can override this with Handler.WithMaxRequestBodySize. A value of 0 means no limit. Default is 1MB (1 << 20).

func (*App) WithMiddleware added in v0.6.1

func (a *App) WithMiddleware(mw func(http.Handler) http.Handler) *App

WithMiddleware adds an HTTP middleware to wrap the app. Middleware is applied in the order added (first added is outermost).

func (*App) WithUnaryInterceptor added in v0.6.1

func (a *App) WithUnaryInterceptor(i UnaryInterceptor) *App

WithUnaryInterceptor adds a global interceptor. Global interceptors are executed before service-level and handler-level interceptors.

Interceptor execution order:

  1. Global interceptors (added via App.WithUnaryInterceptor)
  2. Service interceptors (added via Service.WithUnaryInterceptor)
  3. Handler interceptors (added via Handler.WithUnaryInterceptor)
  4. Handler function

Within each level, interceptors execute in the order they were added.

type CacheConfig

type CacheConfig struct {
	// MaxAge specifies the maximum time a resource is considered fresh (RFC 9111 Section 5.2.2.1).
	// After this time, caches must revalidate before serving the cached response.
	MaxAge time.Duration

	// SMaxAge is like MaxAge but only applies to shared caches like CDNs (RFC 9111 Section 5.2.2.10).
	// Overrides MaxAge for shared caches. Private caches ignore this directive.
	SMaxAge time.Duration

	// StaleWhileRevalidate allows serving stale content while revalidating in the background (RFC 5861).
	// Example: MaxAge=60s, StaleWhileRevalidate=300s means serve from cache for 60s,
	// then serve stale content for up to 300s more while fetching fresh data in background.
	StaleWhileRevalidate time.Duration

	// StaleIfError allows serving stale content if the origin server is unavailable (RFC 5861).
	// Example: StaleIfError=86400 allows serving day-old stale content if origin returns 5xx errors.
	StaleIfError time.Duration

	// Public indicates the response may be cached by any cache, including CDNs (RFC 9111 Section 5.2.2.9).
	// Default is false (private), meaning only the user's browser cache may store it.
	// Set to true for responses that are safe to cache publicly.
	Public bool

	// MustRevalidate requires caches to revalidate stale responses with the origin before serving (RFC 9111 Section 5.2.2.2).
	// Prevents serving stale content. Useful when stale data could cause problems.
	MustRevalidate bool

	// Immutable indicates the response will never change during its freshness lifetime (RFC 8246).
	// Browsers won't send conditional requests for immutable resources within MaxAge period.
	// Useful for content-addressed assets like "bundle.abc123.js".
	Immutable bool
}

CacheConfig defines HTTP cache directives for GET requests. See RFC 9111 (HTTP Caching) for detailed semantics.

Common patterns:

  • Simple caching: CacheConfig{MaxAge: 5*time.Minute}
  • Public CDN caching: CacheConfig{MaxAge: 5*time.Minute, Public: true}
  • Stale-while-revalidate: CacheConfig{MaxAge: 1*time.Minute, StaleWhileRevalidate: 5*time.Minute}
  • Immutable assets: CacheConfig{MaxAge: 365*24*time.Hour, Immutable: true}

type Context added in v0.6.0

type Context interface {
	context.Context

	// Service returns the name of the service being called.
	Service() string

	// EndpointID returns the full identifier for the endpoint being called (e.g., "Users.Create").
	EndpointID() string

	// HTTPRequest returns the underlying HTTP request.
	HTTPRequest() *http.Request

	// HTTPWriter returns the underlying HTTP response writer.
	// Use with caution in handlers - prefer returning errors to writing directly.
	// This is useful for setting response headers.
	HTTPWriter() http.ResponseWriter
}

Context provides type-safe access to request metadata and HTTP primitives. It embeds context.Context, so it can be used anywhere a context.Context is expected.

Interceptors receive Context directly for convenient access to request metadata. Handlers receive context.Context but can use FromContext to get the Context if needed.

For testing interceptors, implement this interface with your own type:

type testContext struct {
    context.Context
    service, method string
}
func (c *testContext) Service() string              { return c.service }
func (c *testContext) EndpointID() string           { return c.service + "." + c.method }
func (c *testContext) HTTPRequest() *http.Request   { return nil }
func (c *testContext) HTTPWriter() http.ResponseWriter { return nil }

func FromContext added in v0.6.0

func FromContext(ctx context.Context) (Context, bool)

FromContext extracts the Context from a context.Context. Returns the Context and true if found, or nil and false if not in a tygor handler context.

This is useful in handlers that receive context.Context but need access to request metadata:

func (s *MyService) GetThing(ctx context.Context, req *GetThingRequest) (*GetThingResponse, error) {
    tc, ok := tygor.FromContext(ctx)
    if ok {
        log.Printf("handling %s", tc.EndpointID())
    }
    // ...
}

type Empty

type Empty *struct{}

Empty represents a void request or response. Use this for operations that don't return meaningful data. The zero value is nil, which serializes to JSON null.

Example:

func DeleteUser(ctx context.Context, req *DeleteUserRequest) (tygor.Empty, error) {
    // ... delete user
    return nil, nil
}

Wire format: {"result": null}

type Endpoint added in v0.7.0

type Endpoint interface {
	// Metadata returns route metadata for code generation.
	// The return type is internal; this method is for use by tygorgen only.
	Metadata() *internal.MethodMetadata
}

Endpoint is the interface for handlers that can be registered with Service.Register.

Implementations:

type Error

type Error struct {
	Code    ErrorCode      `json:"code"`
	Message string         `json:"message"`
	Details map[string]any `json:"details,omitempty"`
}

Error is the standard JSON error envelope.

func DefaultErrorTransformer

func DefaultErrorTransformer(err error) *Error

DefaultErrorTransformer maps standard Go errors to service errors.

func Errorf

func Errorf(code ErrorCode, format string, args ...any) *Error

Errorf creates a new service error with a formatted message.

func NewError

func NewError(code ErrorCode, message string) *Error

NewError creates a new service error.

func (*Error) Error

func (e *Error) Error() string

func (*Error) WithDetail added in v0.6.1

func (e *Error) WithDetail(key string, value any) *Error

WithDetail returns a new Error with the key-value pair added to details.

func (*Error) WithDetails added in v0.6.1

func (e *Error) WithDetails(details map[string]any) *Error

WithDetails returns a new Error with the provided map merged into details. For multiple details, this is more efficient than chaining WithDetail calls.

type ErrorCode

type ErrorCode string

ErrorCode represents a machine-readable error code.

const (
	CodeInvalidArgument   ErrorCode = "invalid_argument"
	CodeUnauthenticated   ErrorCode = "unauthenticated"
	CodePermissionDenied  ErrorCode = "permission_denied"
	CodeNotFound          ErrorCode = "not_found"
	CodeMethodNotAllowed  ErrorCode = "method_not_allowed"
	CodeConflict          ErrorCode = "conflict"
	CodeAlreadyExists     ErrorCode = "already_exists" // Alias for conflict, used when resource already exists
	CodeGone              ErrorCode = "gone"
	CodeResourceExhausted ErrorCode = "resource_exhausted"
	CodeCanceled          ErrorCode = "canceled"
	CodeInternal          ErrorCode = "internal"
	CodeNotImplemented    ErrorCode = "not_implemented"
	CodeUnavailable       ErrorCode = "unavailable"
	CodeDeadlineExceeded  ErrorCode = "deadline_exceeded"
)

func (ErrorCode) HTTPStatus added in v0.6.0

func (c ErrorCode) HTTPStatus() int

HTTPStatus maps an ErrorCode to an HTTP status code.

type ErrorTransformer

type ErrorTransformer func(error) *Error

ErrorTransformer is a function that maps an application error to a service error. If it returns nil, the default transformer logic should be applied.

type ExecHandler added in v0.6.2

type ExecHandler[Req any, Res any] struct {
	// contains filtered or unexported fields
}

ExecHandler implements Endpoint for POST requests (state-changing operations).

Request Type Guidelines:

  • Use struct or pointer types
  • Request is decoded from JSON body

Example:

func CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) { ... }
Exec(CreateUser)

func UpdatePost(ctx context.Context, req *UpdatePostRequest) (*Post, error) { ... }
Exec(UpdatePost).WithUnaryInterceptor(requireAuth)

func Exec added in v0.6.2

func Exec[Req any, Res any](fn func(context.Context, Req) (Res, error)) *ExecHandler[Req, Res]

Exec creates a new POST handler from a generic function for non-streaming API calls.

The handler function signature is func(context.Context, Req) (Res, error). Requests are decoded from JSON body.

For GET requests (cacheable reads), use Query instead.

func (*ExecHandler[Req, Res]) Metadata added in v0.6.2

func (h *ExecHandler[Req, Res]) Metadata() *internal.MethodMetadata

Metadata implements Endpoint.

func (*ExecHandler[Req, Res]) WithMaxRequestBodySize added in v0.6.2

func (h *ExecHandler[Req, Res]) WithMaxRequestBodySize(size uint64) *ExecHandler[Req, Res]

WithMaxRequestBodySize sets the maximum request body size for this handler. This overrides the registry-level default. A value of 0 means no limit.

func (*ExecHandler[Req, Res]) WithSkipValidation added in v0.6.2

func (h *ExecHandler[Req, Res]) WithSkipValidation() *ExecHandler[Req, Res]

WithSkipValidation disables validation for this handler. By default, all handlers validate requests using the validator package. Use this when you need to handle validation manually or when the request type has no validation tags.

func (*ExecHandler[Req, Res]) WithUnaryInterceptor added in v0.6.2

func (h *ExecHandler[Req, Res]) WithUnaryInterceptor(i UnaryInterceptor) *ExecHandler[Req, Res]

WithUnaryInterceptor adds an interceptor to this handler. Handler interceptors execute after global and service interceptors. See App.WithUnaryInterceptor for the complete execution order.

type HandlerFunc

type HandlerFunc func(ctx context.Context, req any) (res any, err error)

HandlerFunc represents the next handler in an interceptor chain. It is passed to UnaryInterceptor functions to invoke the next interceptor or the final handler.

type QueryHandler added in v0.6.2

type QueryHandler[Req any, Res any] struct {
	// contains filtered or unexported fields
}

QueryHandler implements Endpoint for GET requests (cacheable read operations).

Request Type Guidelines:

  • Use struct types for simple cases, pointer types when you need optional fields
  • Request parameters are decoded from URL query string

Struct vs Pointer Types:

  • Struct types (e.g., ListParams): Query parameters are decoded directly into the struct
  • Pointer types (e.g., *ListParams): A new instance is created and query parameters are decoded into it

Example:

func ListPosts(ctx context.Context, req ListPostsParams) ([]*Post, error) { ... }
Query(ListPosts).CacheControl(tygor.CacheConfig{
    MaxAge: 5 * time.Minute,
    Public: true,
})

func Query added in v0.6.2

func Query[Req any, Res any](fn func(context.Context, Req) (Res, error)) *QueryHandler[Req, Res]

Query creates a new GET handler from a generic function for cacheable read operations.

The handler function signature is func(context.Context, Req) (Res, error). Requests are decoded from URL query parameters.

Use CacheControl() to configure HTTP caching behavior.

func (*QueryHandler[Req, Res]) CacheControl added in v0.6.2

func (h *QueryHandler[Req, Res]) CacheControl(cfg CacheConfig) *QueryHandler[Req, Res]

CacheControl sets detailed HTTP cache directives for the handler. See CacheConfig documentation and RFC 9111 for directive semantics.

Example:

Query(ListPosts).CacheControl(tygor.CacheConfig{
    MaxAge:               5 * time.Minute,
    StaleWhileRevalidate: 1 * time.Minute,
    Public:               true,
})
// Sets: Cache-Control: public, max-age=300, stale-while-revalidate=60

func (*QueryHandler[Req, Res]) Metadata added in v0.6.2

func (h *QueryHandler[Req, Res]) Metadata() *internal.MethodMetadata

Metadata implements Endpoint.

func (*QueryHandler[Req, Res]) WithSkipValidation added in v0.6.2

func (h *QueryHandler[Req, Res]) WithSkipValidation() *QueryHandler[Req, Res]

WithSkipValidation disables validation for this handler. By default, all handlers validate requests using the validator package. Use this when you need to handle validation manually or when the request type has no validation tags.

func (*QueryHandler[Req, Res]) WithStrictQueryParams added in v0.6.2

func (h *QueryHandler[Req, Res]) WithStrictQueryParams() *QueryHandler[Req, Res]

WithStrictQueryParams enables strict query parameter validation for GET requests. By default, unknown query parameters are ignored (lenient mode). When enabled, requests with unknown query parameters will return an error. This helps catch typos and enforces exact parameter expectations.

func (*QueryHandler[Req, Res]) WithUnaryInterceptor added in v0.6.2

func (h *QueryHandler[Req, Res]) WithUnaryInterceptor(i UnaryInterceptor) *QueryHandler[Req, Res]

WithUnaryInterceptor adds an interceptor to this handler. Handler interceptors execute after global and service interceptors. See App.WithUnaryInterceptor for the complete execution order.

type Service

type Service struct {
	// contains filtered or unexported fields
}

func (*Service) Register

func (s *Service) Register(name string, handler Endpoint)

Register registers a handler for the given operation name. If a handler is already registered for this service and method, it will be replaced and a warning will be logged.

func (*Service) WithUnaryInterceptor

func (s *Service) WithUnaryInterceptor(i UnaryInterceptor) *Service

WithUnaryInterceptor adds an interceptor to this service. Service interceptors execute after global interceptors but before handler interceptors. See App.WithUnaryInterceptor for the complete execution order.

type UnaryInterceptor

type UnaryInterceptor func(ctx Context, req any, handler HandlerFunc) (res any, err error)

UnaryInterceptor is a hook that wraps handler execution for unary (non-streaming) calls.

Interceptors receive Context for type-safe access to request metadata:

func loggingInterceptor(ctx tygor.Context, req any, handler tygor.HandlerFunc) (any, error) {
    start := time.Now()
    res, err := handler(ctx, req)
    log.Printf("%s took %v", ctx.EndpointID(), time.Since(start))
    return res, err
}

The handler parameter is the next handler in the chain. Interceptors can:

  • Inspect/modify the request before calling handler
  • Inspect/modify the response after calling handler
  • Short-circuit by returning an error without calling handler
  • Add values to context using context.WithValue

req/res are pointers to the request/response structs.

Directories

Path Synopsis
Package devtools provides a devtools service for the tygor vite plugin.
Package devtools provides a devtools service for the tygor vite plugin.
Package discovery provides utilities for serving API discovery documents.
Package discovery provides utilities for serving API discovery documents.
doc
examples/quickstart
Package quickstart provides simple example code for documentation.
Package quickstart provides simple example code for documentation.
examples/tygorgen
Package tygorgen provides example usage for tygorgen documentation.
Package tygorgen provides example usage for tygorgen documentation.
examples module
Package internal contains types for code generation.
Package internal contains types for code generation.
testfixtures
Package testfixtures provides types used for testing the tygorgen package.
Package testfixtures provides types used for testing the tygorgen package.
tgrcontext
Package tgrcontext provides the shared context key for tygor.
Package tgrcontext provides the shared context key for tygor.
tygortest
Package tygortest provides testing helpers for HTTP handlers and tygor service handlers.
Package tygortest provides testing helpers for HTTP handlers and tygor service handlers.
ir
Package ir defines the Intermediate Representation for Go type descriptors.
Package ir defines the Intermediate Representation for Go type descriptors.
provider
Package provider implements input providers for extracting type information from Go code.
Package provider implements input providers for extracting type information from Go code.
sink
Package sink provides output destinations for generated code.
Package sink provides output destinations for generated code.
typescript
Package typescript generates TypeScript type definitions from IR schemas.
Package typescript generates TypeScript type definitions from IR schemas.
typescript/flavor
Package flavor provides the interface and utilities for TypeScript output flavors.
Package flavor provides the interface and utilities for TypeScript output flavors.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL