tygor

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Nov 25, 2025 License: MIT Imports: 14 Imported by: 7

README

tygor

tygor

Type-safe RPC framework for Go with automatic TypeScript client generation.

[!WARNING] tygor is very experimental and the API is rapidly changing. Pinning the @tygor/client version should prevent any unexpected breakages.

Features

  • Type-safe handlers using Go generics
  • Automatic TypeScript generation from Go types
  • Lightweight TypeScript client using proxies for minimal bundle size
  • Zero reflection at runtime for handler execution
  • Request validation with struct tags
  • Flexible error handling with structured error codes
  • Middleware and interceptors at global, service, and handler levels
  • Built-in support for GET and POST methods with appropriate decoding

Philosophy

tygor is for teams building tightly-coupled Go and TypeScript applications in monorepos.

If you're building a fullstack application where the Go backend and TypeScript frontend live together, tygor gives you end-to-end type safety without the ceremony of IDLs like protobuf or OpenAPI specs. You write normal Go functions, and tygor generates TypeScript types that match your actual implementation.

Who is this for?

Use tygor if you:

  • Build fullstack apps with Go + TypeScript in a monorepo
  • Want type safety without learning protobuf/gRPC/OpenAPI
  • Value iteration speed and developer ergonomics
  • Want to write idiomatic Go handlers and get TypeScript types automatically
  • Are okay with incrementally improving types as your domain evolves

Don't use tygor if you:

  • Need a public API with strict backward compatibility guarantees
  • Require multi-language client support (though OpenAPI generation is planned)
  • Need the guarantees of a formal IDL (protobuf, Thrift, etc.)
  • Have microservices that need to evolve independently
The tradeoff

tygor isn't trying to be a perfect code generation tool like protobuf. Instead, it's optimized for the common case: a team iterating on a fullstack app where the backend and frontend are tightly coupled anyway. You can always add handwritten TypeScript definitions to improve type safety for your specific domain. This is often nicer than being forced into the constraints of an IDL.

In the future, tygor may generate OpenAPI specs to enable client generation in other languages, giving you the best of both worlds: ergonomic Go + TypeScript for your core app, with optional compatibility for other ecosystems.

Installation

Go (server-side)
go get github.com/broady/tygor
TypeScript/JavaScript (client-side)
npm install @tygor/client

Or with your preferred package manager:

pnpm add @tygor/client
bun add @tygor/client
yarn add @tygor/client

Quick Start

1. Define your types
package api

type News struct {
    ID        int32      `json:"id"`
    Title     string     `json:"title"`
    Body      *string    `json:"body"`
    CreatedAt *time.Time `json:"created_at"`
}

type ListNewsParams struct {
    Limit  *int32 `json:"limit"`
    Offset *int32 `json:"offset"`
}

type CreateNewsParams struct {
    Title string  `json:"title" validate:"required,min=3"`
    Body  *string `json:"body"`
}
2. Implement handlers
func ListNews(ctx context.Context, req *ListNewsParams) ([]*News, error) {
    // Your implementation
    return news, nil
}

func CreateNews(ctx context.Context, req *CreateNewsParams) (*News, error) {
    // Your implementation
    return &news, nil
}
3. Register services
reg := tygor.NewRegistry()

news := reg.Service("News")
news.Register("List", tygor.UnaryGet(ListNews))
news.Register("Create", tygor.Unary(CreateNews)) // POST is default

http.ListenAndServe(":8080", reg.Handler())
4. Generate TypeScript types
if err := tygorgen.Generate(reg, &tygorgen.Config{
    OutDir: "./client/src/rpc",
}); err != nil {
    log.Fatal(err)
}

This generates TypeScript types and a manifest describing all available RPC methods.

5. Use the TypeScript client

First, install the client runtime:

npm install @tygor/client

The generated client provides a clean, idiomatic API with full type safety:

import { createClient } from '@tygor/client';
import { registry } from './rpc/manifest';

const client = createClient(
  registry,
  {
    baseUrl: 'http://localhost:8080',
    headers: () => ({
      'Authorization': 'Bearer my-token'
    })
    // fetch: customFetch  // Optional: for testing or custom environments
  }
);

// Type-safe calls with autocomplete
const news = await client.News.List({ limit: 10, offset: 0 });
// news: News[]

const created = await client.News.Create({
  title: "Breaking News",
  body: "Important update"
});
// created: News

// Errors are properly typed with a hierarchy:
// - TygorError (base class)
//   - RPCError: application errors from the server (has code, message, details)
//   - TransportError: network/proxy errors (has httpStatus, rawBody)
try {
  await client.News.Create({ title: "x" }); // Validation error
} catch (err) {
  if (err instanceof RPCError) {
    console.error(err.code, err.message); // "invalid_argument", "validation failed"
    console.error(err.details);           // Additional error context
  } else if (err instanceof TransportError) {
    console.error("Transport error:", err.httpStatus);
  }
}
// See doc/TYPESCRIPT-CLIENT.md for detailed error handling patterns.

The client uses JavaScript Proxies to provide method access without code generation bloat. Your bundle only includes the types and a small runtime, regardless of how many RPC methods you have.

Example manifest.ts:

export interface RPCManifest {
  "News.List": {
    req: types.ListNewsParams;
    res: types.News[];
  };
  "News.Create": {
    req: types.CreateNewsParams;
    res: types.News;
  };
}

const metadata = {
  "News.List": { method: "GET", path: "/News/List" },
  "News.Create": { method: "POST", path: "/News/Create" },
} as const;

export const registry: ServiceRegistry<RPCManifest> = {
  manifest: {} as RPCManifest,
  metadata,
};

Request Handling

GET Requests

For GET requests, parameters are decoded from query strings:

type ListParams struct {
    Limit  *int32 `json:"limit"`
    Offset *int32 `json:"offset"`
}

tygor.UnaryGet(List)

Query: /News/List?limit=10&offset=20

POST Requests

For POST requests (the default), the body is decoded as JSON:

type CreateParams struct {
    Title string `json:"title" validate:"required"`
}

tygor.Unary(Create) // POST is default

Error Handling

Use structured error codes for consistent error responses:

func CreateNews(ctx context.Context, req *CreateNewsParams) (*News, error) {
    if req.Title == "invalid" {
        return nil, tygor.NewError(tygor.CodeInvalidArgument, "invalid title")
    }
    return &news, nil
}

Available error codes:

  • CodeOK (200)
  • CodeInvalidArgument (400)
  • CodeUnauthenticated (401)
  • CodePermissionDenied (403)
  • CodeNotFound (404)
  • CodeAlreadyExists (409)
  • CodeResourceExhausted (429)
  • CodeInternal (500)
  • CodeUnavailable (503)
Custom Error Transformers

Map application errors to RPC errors:

reg := tygor.NewRegistry().
    WithErrorTransformer(func(err error) *tygor.Error {
        if errors.Is(err, sql.ErrNoRows) {
            return tygor.NewError(tygor.CodeNotFound, "not found")
        }
        return nil
    })
Masking Internal Errors

Prevent sensitive error details from leaking in production:

reg := tygor.NewRegistry().WithMaskInternalErrors()

Interceptors

Interceptors provide cross-cutting concerns at different levels.

Global Interceptors

Applied to all handlers:

reg := tygor.NewRegistry().
    WithInterceptor(middleware.LoggingInterceptor(logger))
Service Interceptors

Applied to all handlers in a service:

news := reg.Service("News").
    WithInterceptor(authInterceptor)
Handler Interceptors

Applied to specific handlers:

news.Register("Create",
    tygor.Unary(CreateNews).
        WithInterceptor(func(ctx context.Context, req any, info *tygor.RPCInfo, handler tygor.HandlerFunc) (any, error) {
            // Custom logic
            return handler(ctx, req)
        }))

Middleware

HTTP middleware wraps the entire registry:

reg := tygor.NewRegistry().
    WithMiddleware(middleware.CORS(middleware.DefaultCORSConfig()))

http.ListenAndServe(":8080", reg.Handler())

Validation

POST Requests

POST request bodies are validated using struct tags via the validator/v10 package:

type CreateParams struct {
    Title string `json:"title" validate:"required,min=3,max=100"`
    Email string `json:"email" validate:"required,email"`
}
GET Requests

GET request query parameters are decoded using gorilla/schema and then validated with validator/v10:

type ListParams struct {
    Limit  int    `schema:"limit" validate:"min=0,max=100"`
    Offset int    `schema:"offset" validate:"min=0"`
    Status string `schema:"status" validate:"omitempty,oneof=draft published"`
}

Query: /News/List?limit=10&offset=0&status=published

Note: gorilla/schema uses case-insensitive matching for query parameter names. Without a schema tag, the field name is used (e.g., field Limit matches query param limit, Limit, or LIMIT). For clarity, always use explicit schema tags.

Caching

Set cache headers on GET handlers using CacheControl:

news.Register("List",
    tygor.UnaryGet(ListNews).
        CacheControl(tygor.CacheConfig{
            MaxAge: 5 * time.Minute,
            Public: true,
        }))

Common patterns:

// Browser-only caching (private)
CacheControl(tygor.CacheConfig{MaxAge: 5 * time.Minute})

// CDN + browser caching (public)
CacheControl(tygor.CacheConfig{MaxAge: 5 * time.Minute, Public: true})

// Stale-while-revalidate for smooth updates
CacheControl(tygor.CacheConfig{
    MaxAge:               1 * time.Minute,
    StaleWhileRevalidate: 5 * time.Minute,
    Public:               true,
})

Context Helpers

Access request metadata and modify responses:

func Handler(ctx context.Context, req *Request) (*Response, error) {
    // Get service and method name
    service, method, _ := tygor.MethodFromContext(ctx)

    // Set custom response headers
    tygor.SetHeader(ctx, "X-Custom", "value")

    return &Response{}, nil
}

Type Mappings

Customize TypeScript type generation for third-party types:

tygorgen.Generate(reg, &tygorgen.Config{
    OutDir: "./client/src/rpc",
    TypeMappings: map[string]string{
        "github.com/jackc/pgtype.Timestamptz": "string | null",
        "github.com/jackc/pgtype.UUID":        "string",
    },
})

License

MIT

Tiger image by Yan Liu, licensed under CC-BY (with a few modifications).

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func HTTPStatusFromCode

func HTTPStatusFromCode(code ErrorCode) int

HTTPStatusFromCode maps an ErrorCode to an HTTP status code.

func MethodFromContext

func MethodFromContext(ctx context.Context) (service, method string, ok bool)

MethodFromContext returns the service and method name of the current RPC.

func NewTestContext

func NewTestContext(ctx context.Context, w http.ResponseWriter, r *http.Request, info *RPCInfo) context.Context

NewTestContext creates a context with RPC metadata for testing. This is useful when testing handlers directly without going through the Registry.

Example:

req := httptest.NewRequest("POST", "/test", body)
w := httptest.NewRecorder()
info := &RPCInfo{Service: "MyService", Method: "MyMethod"}
ctx := NewTestContext(req.Context(), w, req, info)
req = req.WithContext(ctx)

func RequestFromContext

func RequestFromContext(ctx context.Context) *http.Request

RequestFromContext returns the HTTP request from the context.

func SetHeader

func SetHeader(ctx context.Context, key, value string)

SetHeader sets an HTTP response header. It requires that the handler was called via the Registry.

func TestContextSetup

func TestContextSetup() func(ctx context.Context, w http.ResponseWriter, r *http.Request, service, method string) context.Context

TestContextSetup returns a context setup function for use with testutil.NewRequest(). This provides a convenient way to set up tygor RPC context when testing from external packages.

Example usage:

req, w := testutil.NewRequest(tygor.TestContextSetup()).
    POST("/test").
    WithJSON(&MyRequest{...}).
    Build()

handler.ServeHTTP(w, req, tygor.HandlerConfig{})

Types

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 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 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 AlreadyExists added in v0.5.0

func AlreadyExists(message string, details ...map[string]any) *Error

AlreadyExists creates an already_exists error (409).

func Conflict added in v0.5.0

func Conflict(message string, details ...map[string]any) *Error

Conflict creates a conflict error (409).

func DeadlineExceeded added in v0.5.0

func DeadlineExceeded(message string, details ...map[string]any) *Error

DeadlineExceeded creates a deadline_exceeded error (504).

func DefaultErrorTransformer

func DefaultErrorTransformer(err error) *Error

DefaultErrorTransformer maps standard Go errors to RPC errors.

func Errorf

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

Errorf creates a new RPC error with a formatted message.

func Gone added in v0.5.0

func Gone(message string, details ...map[string]any) *Error

Gone creates a gone error (410).

func Internal added in v0.5.0

func Internal(message string, details ...map[string]any) *Error

Internal creates an internal error (500).

func InvalidArgument added in v0.5.0

func InvalidArgument(message string, details ...map[string]any) *Error

InvalidArgument creates an invalid_argument error (400).

func NewError

func NewError(code ErrorCode, message string) *Error

NewError creates a new RPC error.

func NotFound added in v0.5.0

func NotFound(message string, details ...map[string]any) *Error

NotFound creates a not_found error (404).

func NotImplemented added in v0.5.0

func NotImplemented(message string, details ...map[string]any) *Error

NotImplemented creates a not_implemented error (501).

func PermissionDenied added in v0.5.0

func PermissionDenied(message string, details ...map[string]any) *Error

PermissionDenied creates a permission_denied error (403).

func ResourceExhausted added in v0.5.0

func ResourceExhausted(message string, details ...map[string]any) *Error

ResourceExhausted creates a resource_exhausted error (429).

func Unauthenticated added in v0.5.0

func Unauthenticated(message string, details ...map[string]any) *Error

Unauthenticated creates an unauthenticated error (401).

func Unavailable added in v0.5.0

func Unavailable(message string, details ...map[string]any) *Error

Unavailable creates an unavailable error (503).

func (*Error) Error

func (e *Error) Error() string

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

type ErrorTransformer

type ErrorTransformer func(error) *Error

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

type ExportedRoute

type ExportedRoute struct {
	Name     string
	Request  reflect.Type
	Response reflect.Type
	Method   string
}

ExportedRoute contains metadata about a registered route for code generation.

type HandlerConfig

type HandlerConfig struct {
	ErrorTransformer   ErrorTransformer
	MaskInternalErrors bool
	Interceptors       []UnaryInterceptor
	Logger             *slog.Logger
	MaxRequestBodySize uint64
}

HandlerConfig contains configuration passed from Registry to handlers.

type HandlerFunc

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

HandlerFunc represents the next handler in the chain.

type RPCInfo

type RPCInfo struct {
	Service string
	Method  string
}

RPCInfo provides metadata about the current operation.

type RPCMethod

type RPCMethod interface {
	ServeHTTP(w http.ResponseWriter, r *http.Request, config HandlerConfig)
	Metadata() *meta.MethodMetadata
}

RPCMethod is the interface for registered handlers. It is exported so users can pass it to Register, but sealed so they cannot implement it.

type Registry

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

func NewRegistry

func NewRegistry() *Registry

func (*Registry) ExportRoutes

func (r *Registry) ExportRoutes() map[string]ExportedRoute

ExportRoutes returns all registered routes for code generation purposes. This is used by the generator package.

func (*Registry) Handler

func (r *Registry) Handler() http.Handler

Handler returns the registry wrapped with all configured middleware. The middleware is applied in the order it was added via WithMiddleware.

func (*Registry) ServeHTTP

func (r *Registry) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP implements http.Handler.

func (*Registry) Service

func (r *Registry) Service(name string) *Service

Service returns a Service namespace.

func (*Registry) WithErrorTransformer

func (r *Registry) WithErrorTransformer(fn ErrorTransformer) *Registry

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

func (*Registry) WithLogger

func (r *Registry) WithLogger(logger *slog.Logger) *Registry

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

func (*Registry) WithMaskInternalErrors

func (r *Registry) WithMaskInternalErrors() *Registry

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 (*Registry) WithMaxRequestBodySize

func (r *Registry) WithMaxRequestBodySize(size uint64) *Registry

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 (*Registry) WithMiddleware

func (r *Registry) WithMiddleware(mw func(http.Handler) http.Handler) *Registry

WithMiddleware adds an HTTP middleware to wrap the registry. Middleware is applied in the order added (first added is outermost). Use Handler() to get the wrapped handler.

func (*Registry) WithUnaryInterceptor

func (r *Registry) WithUnaryInterceptor(i UnaryInterceptor) *Registry

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

Interceptor execution order:

  1. Global interceptors (added via Registry.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 Service

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

func (*Service) Register

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

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 Registry.WithUnaryInterceptor for the complete execution order.

type UnaryGetHandler added in v0.5.0

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

UnaryGetHandler implements RPCMethod for GET requests (cacheable read operations).

It embeds UnaryHandler, inheriting methods like WithUnaryInterceptor and WithSkipValidation.

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) { ... }
UnaryGet(ListPosts).CacheControl(tygor.CacheConfig{
    MaxAge: 5 * time.Minute,
    Public: true,
})

func UnaryGet

func UnaryGet[Req any, Res any](fn func(context.Context, Req) (Res, error)) *UnaryGetHandler[Req, Res]

UnaryGet 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.

The returned UnaryGetHandler supports:

  • WithUnaryInterceptor (from UnaryHandler)
  • WithSkipValidation (from UnaryHandler)
  • CacheControl (specific to GET)
  • WithStrictQueryParams (specific to GET)

func (*UnaryGetHandler[Req, Res]) CacheControl added in v0.5.0

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

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

Example:

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

func (*UnaryGetHandler[Req, Res]) Metadata added in v0.5.0

func (h *UnaryGetHandler[Req, Res]) Metadata() *meta.MethodMetadata

Metadata returns the runtime metadata for the GET handler.

func (*UnaryGetHandler[Req, Res]) ServeHTTP added in v0.5.0

func (h *UnaryGetHandler[Req, Res]) ServeHTTP(w http.ResponseWriter, r *http.Request, config HandlerConfig)

ServeHTTP implements the RPC handler for GET requests with caching support.

func (*UnaryGetHandler[Req, Res]) WithSkipValidation added in v0.5.0

func (h *UnaryGetHandler[Req, Res]) WithSkipValidation() *UnaryGetHandler[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 (*UnaryGetHandler[Req, Res]) WithStrictQueryParams added in v0.5.0

func (h *UnaryGetHandler[Req, Res]) WithStrictQueryParams() *UnaryGetHandler[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 (*UnaryGetHandler[Req, Res]) WithUnaryInterceptor added in v0.5.0

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

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

type UnaryHandler added in v0.5.0

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

UnaryHandler contains common configuration for all unary handlers.

This is a base type. See UnaryPostHandler and UnaryGetHandler for specific implementations.

func (*UnaryHandler[Req, Res]) Metadata added in v0.5.0

func (h *UnaryHandler[Req, Res]) Metadata() *meta.MethodMetadata

Metadata returns the runtime metadata for the handler.

type UnaryInterceptor

type UnaryInterceptor func(ctx context.Context, req any, info *RPCInfo, handler HandlerFunc) (res any, err error)

UnaryInterceptor is a generic hook that wraps the RPC handler execution for unary (non-streaming) calls. req/res are pointers to the structs.

type UnaryPostHandler added in v0.5.0

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

UnaryPostHandler implements RPCMethod for POST requests (state-changing operations).

It embeds UnaryHandler, inheriting methods like WithUnaryInterceptor and WithSkipValidation.

Request Type Guidelines:

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

Example:

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

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

func Unary

func Unary[Req any, Res any](fn func(context.Context, Req) (Res, error)) *UnaryPostHandler[Req, Res]

Unary creates a new POST handler from a generic function for unary (non-streaming) RPCs.

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

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

The returned UnaryPostHandler supports:

  • WithUnaryInterceptor (from UnaryHandler)
  • WithSkipValidation (from UnaryHandler)
  • WithMaxRequestBodySize (specific to POST)

func (*UnaryPostHandler[Req, Res]) Metadata added in v0.5.0

func (h *UnaryPostHandler[Req, Res]) Metadata() *meta.MethodMetadata

Metadata returns the runtime metadata for the POST handler.

func (*UnaryPostHandler[Req, Res]) ServeHTTP added in v0.5.0

func (h *UnaryPostHandler[Req, Res]) ServeHTTP(w http.ResponseWriter, r *http.Request, config HandlerConfig)

ServeHTTP implements the RPC handler for POST requests.

func (*UnaryPostHandler[Req, Res]) WithMaxRequestBodySize added in v0.5.0

func (h *UnaryPostHandler[Req, Res]) WithMaxRequestBodySize(size uint64) *UnaryPostHandler[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 (*UnaryPostHandler[Req, Res]) WithSkipValidation added in v0.5.0

func (h *UnaryPostHandler[Req, Res]) WithSkipValidation() *UnaryPostHandler[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 (*UnaryPostHandler[Req, Res]) WithUnaryInterceptor added in v0.5.0

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

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

Directories

Path Synopsis
examples module
blog command
newsserver command
internal
Package testutil provides testing helpers for HTTP handlers and tygor RPC handlers.
Package testutil provides testing helpers for HTTP handlers and tygor RPC handlers.

Jump to

Keyboard shortcuts

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