tygor

package module
v0.8.3 Latest Latest
Warning

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

Go to latest
Published: Dec 4, 2025 License: MIT Imports: 17 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;
}

Call your API with full type safety:

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

Try it now

Clone the React + Vite example and start building:

bunx degit broady/tygor/examples/react#v0.8.3 my-app
cd my-app && bun i && bun dev

Or with npm: npx degit broady/tygor/examples/react#v0.8.3 my-app

Prerequisites: Go 1.21+, Node.js 18+

Quick Start

If you prefer to start from scratch rather than the example above:

1. Create your app with an export function

The CLI finds your app by scanning for any exported function that returns *tygor.App:

func SetupApp() *tygor.App {
	app := tygor.NewApp()

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

	return app
}

  • Query handlers serve GET requests (query params in URL)
  • Exec handlers serve POST requests (JSON body)
app := SetupApp()
http.ListenAndServe(":8080", app.Handler())
2. Generate TypeScript
# Generate types and manifest
tygor gen ./src/rpc

# With Zod schemas for client-side validation
tygor gen ./src/rpc --flavor zod

This creates:

  • types.ts - TypeScript interfaces for all your Go types
  • manifest.ts - Service registry for the client
  • schemas.zod.ts - Zod schemas (with --flavor zod)
3. Call from TypeScript
import { createClient } from "@tygor/client";
import { registry } from "./rpc/manifest";

const client = createClient(registry, {
  baseUrl: "http://localhost:8080",
});

// Type-safe API calls
const user = await client.Users.Get({ id: 123 });
console.log(user.name); // autocomplete works

Development with Vite

The Vite plugin gives you hot reload, error overlay, and devtools:

npm install @tygor/vite-plugin
import { defineConfig } from "vite";
import { tygor } from "@tygor/vite-plugin";

export default defineConfig({
  plugins: [
    tygor({
      workdir: "../server",  // Path to your Go module
      build: "go build -o ./tmp/server .",
      start: (port) => ({ cmd: `./tmp/server -port=${port}` }),
    }),
  ],
});

The plugin automatically:

  • Runs tygor gen on startup and file changes
  • Hot-reloads your Go server with zero downtime
  • Shows build errors in-browser
  • Provides a devtools sidebar with API info

Your frontend state persists across server reloads. See @tygor/vite-plugin for full docs.

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 → OpenAPI generators
  • Need strict public API contracts → Connect/gRPC with protobuf

Features

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"}

Validation errors return CodeInvalidArgument automatically when validate tags fail.

Validation

Server-side validation with validator/v10 tags, 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 with Zod.

Caching

Cache control for Query (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)

Execution order: app → service → handler → your function.

Middleware

Standard HTTP middleware:

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

CLI Reference

tygor gen <outdir>           # Generate TypeScript types and manifest
  -p, --package    Package to scan (default: current directory)
  -f, --flavor     Add Zod schemas: zod, zod-mini
  -d, --discovery  Generate discovery.json for runtime introspection
  -c, --check      Check if files are up-to-date (for CI)

tygor check                  # Validate exports without generating

tygor dev                    # Start devtools server (used by Vite plugin)
  --rpc-dir        Directory with discovery.json
  --port           Server port (default: 9000)

Advanced: Programmatic Generation

You can also generate types programmatically, useful for custom build scripts:

tygorgen.FromApp(app).ToDir("./client/src/rpc")

Examples

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.

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

View Source
var ErrStreamClosed = errors.New("stream closed")

ErrStreamClosed is returned by StreamWriter.Send when the client has disconnected or the stream has been closed. Handlers should return when they receive this error.

View Source
var ErrWriteTimeout = errors.New("write timeout")

ErrWriteTimeout is returned by StreamWriter.Send when a write to the client timed out. This typically indicates a slow or unresponsive client.

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) WithStreamHeartbeat added in v0.7.3

func (a *App) WithStreamHeartbeat(d time.Duration) *App

WithStreamHeartbeat sets the default interval for sending SSE heartbeat comments. Heartbeats keep connections alive through proxies with idle timeouts. Individual handlers can override this with StreamHandler.WithHeartbeat.

Default is 30 seconds. Use 0 to disable heartbeats.

func (*App) WithStreamWriteTimeout added in v0.7.3

func (a *App) WithStreamWriteTimeout(d time.Duration) *App

WithStreamWriteTimeout sets the default timeout for writing SSE events. If a single event write takes longer than this, the stream is closed. Individual handlers can override this with StreamHandler.WithWriteTimeout.

Default is 30 seconds. Use 0 to disable (not recommended - risks goroutine leaks).

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 Atom added in v0.8.0

type Atom[T any] struct {
	// contains filtered or unexported fields
}

Atom holds a single value that can be read, written, and subscribed to. Updates are broadcast to all subscribers via SSE streaming. Thread-safe for concurrent Get/Set operations.

Unlike event streams, Atom represents current state - subscribers always receive the latest value, and intermediate updates may be skipped if a subscriber is slow.

Example:

status := tygor.NewAtom(&Status{State: "idle"})

// Read current value
current := status.Get()

// Update and broadcast to all subscribers
status.Set(&Status{State: "running"})

// Register SSE endpoint with proper "atom" primitive
svc.Register("Status", status.Handler())

func NewAtom added in v0.8.0

func NewAtom[T any](initial T) *Atom[T]

NewAtom creates a new Atom with the given initial value.

func (*Atom[T]) Close added in v0.8.0

func (a *Atom[T]) Close()

Close signals all subscribers to disconnect and prevents new subscriptions. Safe to call multiple times. After Close, Set is a no-op.

func (*Atom[T]) Get added in v0.8.0

func (a *Atom[T]) Get() T

Get returns the current value.

func (*Atom[T]) Handler added in v0.8.0

func (a *Atom[T]) Handler() *AtomHandler[T]

Handler returns an AtomHandler for registering with a Service. The handler uses the "atom" primitive for proper TypeScript codegen.

Example:

svc.Register("Status", statusAtom.Handler())

func (*Atom[T]) Set added in v0.8.0

func (a *Atom[T]) Set(value T)

Set updates the value and broadcasts to all subscribers. The value is serialized once and the same bytes are sent to all subscribers. No-op if the Atom has been closed.

func (*Atom[T]) Subscribe added in v0.8.0

func (a *Atom[T]) Subscribe(ctx context.Context) iter.Seq[T]

Subscribe returns an iterator that yields the current value and all future updates until ctx is canceled or the Atom is closed. For use in Go code, not HTTP handlers.

func (*Atom[T]) Update added in v0.8.0

func (a *Atom[T]) Update(fn func(T) T)

Update atomically applies fn to the current value. Useful for read-modify-write operations.

type AtomHandler added in v0.8.0

type AtomHandler[T any] struct {
	// contains filtered or unexported fields
}

AtomHandler implements Endpoint for Atom subscriptions. It streams the current value immediately, then pushes updates via SSE.

func (*AtomHandler[T]) Metadata added in v0.8.0

func (h *AtomHandler[T]) Metadata() *internal.MethodMetadata

Metadata implements Endpoint.

func (*AtomHandler[T]) WithHeartbeat added in v0.8.0

func (h *AtomHandler[T]) WithHeartbeat(d time.Duration) *AtomHandler[T]

WithHeartbeat sets the interval for sending SSE heartbeat comments.

func (*AtomHandler[T]) WithUnaryInterceptor added in v0.8.0

func (h *AtomHandler[T]) WithUnaryInterceptor(i UnaryInterceptor) *AtomHandler[T]

WithUnaryInterceptor adds an interceptor that runs during stream setup.

func (*AtomHandler[T]) WithWriteTimeout added in v0.8.0

func (h *AtomHandler[T]) WithWriteTimeout(d time.Duration) *AtomHandler[T]

WithWriteTimeout sets the timeout for writing each event to the client.

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 StreamHandler added in v0.7.3

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

StreamHandler implements Endpoint for SSE streaming responses.

Stream handlers return an iterator that yields events to the client. The connection stays open until the iterator is exhausted, an error occurs, or the client disconnects.

Example:

func SubscribeToFeed(ctx context.Context, req *SubscribeRequest) iter.Seq2[*FeedEvent, error] {
    return func(yield func(*FeedEvent, error) bool) {
        ticker := time.NewTicker(time.Second)
        defer ticker.Stop()
        for {
            select {
            case <-ctx.Done():
                return
            case <-ticker.C:
                if !yield(&FeedEvent{Time: time.Now()}, nil) {
                    return
                }
            }
        }
    }
}

feed.Register("Subscribe", tygor.Stream(SubscribeToFeed))

func Stream added in v0.7.3

func Stream[Req any, Res any](fn func(context.Context, Req, StreamWriter[Res]) error) *StreamHandler[Req, Res]

Stream creates a new SSE streaming handler from a callback function.

The handler receives a StreamWriter to send events to the client. StreamWriter.Send returns an error when the stream should stop:

  • Client disconnects
  • Context is canceled or times out
  • Write fails

All disconnect-related errors satisfy errors.Is(err, ErrStreamClosed). For finer distinction, you can also check errors.Is(err, context.Canceled) or errors.Is(err, context.DeadlineExceeded).

Handlers should return when Send returns an error. Any error returned by the handler (except ErrStreamClosed) is sent to the client as a final error event.

Example:

func Subscribe(ctx context.Context, req *SubscribeRequest, stream tygor.StreamWriter[*FeedEvent]) error {
    // Check for reconnection
    if lastID := stream.LastEventID(); lastID != "" {
        // Resume from lastID
    }

    sub := broker.Subscribe(req.Topic)
    defer sub.Close()

    for {
        select {
        case <-ctx.Done():
            return nil
        case event := <-sub.Events():
            if err := stream.Send(event); err != nil {
                return err
            }
            // Or with event ID for reconnection support:
            // if err := stream.SendWithID(event.ID, event); err != nil { ... }
        }
    }
}

feed.Register("Subscribe", tygor.Stream(Subscribe))

func (*StreamHandler[Req, Res]) Metadata added in v0.7.3

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

Metadata implements Endpoint.

func (*StreamHandler[Req, Res]) WithHeartbeat added in v0.7.3

func (h *StreamHandler[Req, Res]) WithHeartbeat(d time.Duration) *StreamHandler[Req, Res]

WithHeartbeat sets the interval for sending SSE heartbeat comments. This keeps connections alive through proxies with idle timeouts.

Heartbeats are sent as SSE comments (": heartbeat\n\n") which are ignored by the EventSource API but reset idle timers on proxies.

Default is 30 seconds. Use 0 to disable heartbeats.

func (*StreamHandler[Req, Res]) WithMaxRequestBodySize added in v0.7.3

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

WithMaxRequestBodySize sets the maximum request body size for this handler.

func (*StreamHandler[Req, Res]) WithSkipValidation added in v0.7.3

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

WithSkipValidation disables request validation for this handler.

func (*StreamHandler[Req, Res]) WithStreamInterceptor added in v0.7.3

func (h *StreamHandler[Req, Res]) WithStreamInterceptor(i StreamInterceptor) *StreamHandler[Req, Res]

WithStreamInterceptor adds an interceptor that wraps the event stream. Stream interceptors can transform, filter, or observe events.

func (*StreamHandler[Req, Res]) WithUnaryInterceptor added in v0.7.3

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

WithUnaryInterceptor adds an interceptor that runs during stream setup. Unary interceptors execute before the stream starts, useful for auth checks. They do not see the stream response (it doesn't exist yet).

func (*StreamHandler[Req, Res]) WithWriteTimeout added in v0.7.3

func (h *StreamHandler[Req, Res]) WithWriteTimeout(d time.Duration) *StreamHandler[Req, Res]

WithWriteTimeout sets the timeout for writing each event to the client. If a write takes longer than this duration, the stream is closed and emit returns ErrWriteTimeout.

A zero duration means no timeout (the default).

type StreamHandlerFunc added in v0.7.3

type StreamHandlerFunc func(ctx context.Context, req any) iter.Seq2[any, error]

StreamHandlerFunc represents the next handler in a stream interceptor chain.

type StreamInterceptor added in v0.7.3

type StreamInterceptor func(ctx Context, req any, handler StreamHandlerFunc) iter.Seq2[any, error]

StreamInterceptor wraps stream execution.

Unlike UnaryInterceptor which wraps a single request/response, StreamInterceptor wraps the entire event stream. It can:

  • Transform or filter events
  • Add logging for stream lifecycle
  • Implement backpressure or rate limiting

Example:

func loggingStreamInterceptor(ctx tygor.Context, req any, handler tygor.StreamHandlerFunc) iter.Seq2[any, error] {
    start := time.Now()
    events := handler(ctx, req)
    return func(yield func(any, error) bool) {
        count := 0
        for event, err := range events {
            count++
            if !yield(event, err) {
                break
            }
        }
        log.Printf("%s streamed %d events in %v", ctx.EndpointID(), count, time.Since(start))
    }
}

type StreamWriter added in v0.8.0

type StreamWriter[T any] interface {
	// Send sends an event to the client.
	// Returns an error if the client has disconnected or the context is canceled.
	// All disconnect-related errors satisfy errors.Is(err, [ErrStreamClosed]).
	Send(event T) error

	// SendWithID sends an event with an SSE event ID.
	// The ID is sent as the "id:" field in the SSE stream, allowing clients
	// to resume from this point on reconnection via the Last-Event-ID header.
	SendWithID(id string, event T) error

	// LastEventID returns the client's Last-Event-ID header value.
	// This is set when the client reconnects after a disconnection.
	// Returns empty string on first connection or if the client didn't send the header.
	LastEventID() string
}

StreamWriter sends events to a streaming client. It provides methods for sending events with optional SSE event IDs and for checking the client's last received event ID on reconnection.

This interface enables testing stream handlers without a real HTTP connection:

type mockStreamWriter[T any] struct {
    events []T
}
func (m *mockStreamWriter[T]) Send(event T) error { m.events = append(m.events, event); return nil }
func (m *mockStreamWriter[T]) SendWithID(id string, event T) error { return m.Send(event) }
func (m *mockStreamWriter[T]) LastEventID() string { return "" }

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
cmd
tygor command
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.
discover
Package discover finds tygor export functions by signature.
Package discover finds tygor export functions by signature.
runner
Package runner executes tygor code generation by building and running a modified version of the user's package.
Package runner executes tygor code generation by building and running a modified version of the user's package.
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