httprpc

package module
v0.0.0-...-25b0c30 Latest Latest
Warning

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

Go to latest
Published: Dec 20, 2025 License: MIT Imports: 22 Imported by: 0

README

httprpc

httprpc is a Go library for building typed HTTP RPC services with reflection-based TypeScript client generation.

Installation

go get github.com/behzade/httprpc

Quick Start

package main

import (
    "context"
    "net/http"

    "github.com/behzade/httprpc"
)

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    r := httprpc.New()

    httprpc.RegisterHandler(r.EndpointGroup, httprpc.POST(
        func(ctx context.Context, req CreateUserRequest) (User, error) {
            // Your business logic here
            return User{ID: 1, Name: req.Name, Email: req.Email}, nil
        },
        "/users",
    ))

http.ListenAndServe(":8080", r.HandlerMust())
}

Core Concepts

Handlers

Handlers are typed functions that take a context and a request type, returning a response type and an error:

type Handler[Req any, Res any] func(ctx context.Context, request Req) (Res, error)

For endpoints that need typed path/header metadata, use HandlerWithMeta:

type HandlerWithMeta[Req any, Meta any, Res any] func(ctx context.Context, request Req, meta Meta) (Res, error)
Endpoints

Endpoints combine a handler with an HTTP method and path:

endpoint := httprpc.POST(handler, "/path")

Supported methods: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD.

For meta-aware handlers, use GETM/POSTM and RegisterHandlerM.

Path params

You can register routes with path parameters using :name segments (snake_case):

Route paths are normalized by trimming leading/trailing slashes, so /users and /users/ are equivalent.

type GetUserMeta struct {
	ID int `path:"id"`
}

httprpc.RegisterHandlerM(router.EndpointGroup, httprpc.GETM(
	func(ctx context.Context, _ struct{}, meta GetUserMeta) (User, error) {
		return userService.Get(ctx, meta.ID)
	},
	"/users/:id",
))

Path parameters are decoded into the meta struct using the path tag (snake_case). They are not merged into the request body/query. You can also read them directly via httprpc.PathParam(ctx, "id") if you need access in untyped middleware.

Meta structs can also decode headers:

type AuthMeta struct {
	Authorization string `header:"authorization"`
	RequestID     string `header:"x-request-id,omitempty"`
}

httprpc.RegisterHandlerM(router.EndpointGroup, httprpc.GETM(
	func(ctx context.Context, _ struct{}, meta AuthMeta) (User, error) {
		return userService.GetAuthorized(ctx, meta.Authorization)
	},
	"/me",
))

Header fields without omitempty are required; missing headers return 400 Bad Request.

Registration

Register endpoints on a router or endpoint group:

httprpc.RegisterHandler(router.EndpointGroup, endpoint)
// or
group := router.Group("/api")
httprpc.RegisterHandler(group, endpoint)

For meta-aware endpoints, use RegisterHandlerM.

Router

The router manages endpoints and provides the HTTP handler:

r := httprpc.New()
// Register endpoints...
handler := r.HandlerMust()
Server

For convenience, create a configured http.Server:

server := r.Server(":8080")
server.ListenAndServe()

Or use RunServer for automatic graceful shutdown on SIGINT/SIGTERM:

// Simple usage with defaults (graceful shutdown with 30s timeout)
if err := r.RunServer(":8080"); err != nil {
    log.Fatal(err)
}

// Custom shutdown timeout
r.RunServer(":8080", httprpc.WithGracefulShutdown(60*time.Second))

// Custom logger
r.RunServer(":8080", httprpc.WithLogger(myLogger))

// Combine options
r.RunServer(":8080",
    httprpc.WithGracefulShutdown(60*time.Second),
    httprpc.WithLogger(myLogger),
)

Middleware

Untyped Middleware

Apply HTTP-level middleware to routers or groups:

r.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Middleware logic
        next.ServeHTTP(w, r)
    })
})

Middleware priority controls execution order (higher priority runs earlier):

r.Use(middleware, httprpc.Priority(10))

Built-in middlewares (in github.com/behzade/httprpc/middleware):

  • Recover(logger) – panic recovery with 500 fallback.
  • Logging(logger) – request/response logging (includes request ID when set).
  • RequestID(header) – propagates/generates request IDs (default header: X-Request-ID).
  • RequestSizeLimit(maxBytes) – wraps http.MaxBytesReader.
  • Timeout(d) – adds a per-request context timeout.
  • CORS(cfg) – simple configurable CORS handling.
Typed Middleware

Apply per-endpoint typed middleware:

httprpc.RegisterHandler(r, endpoint, httprpc.WithMiddleware[Req, Res](func(next httprpc.Handler[Req, Res]) httprpc.Handler[Req, Res] {
    return func(ctx context.Context, req Req) (Res, error) {
        // Typed middleware logic
        return next(ctx, req)
    }
}))

For meta-aware handlers, use WithMetaMiddleware and HandlerWithMeta:

httprpc.RegisterHandlerM(r, endpoint, httprpc.WithMetaMiddleware[Req, Meta, Res](func(next httprpc.HandlerWithMeta[Req, Meta, Res]) httprpc.HandlerWithMeta[Req, Meta, Res] {
	return func(ctx context.Context, req Req, meta Meta) (Res, error) {
		// Typed middleware logic with meta
		return next(ctx, req, meta)
	}
}))

Endpoint Groups

Organize endpoints with groups and prefixes:

api := r.Group("/api")
v1 := api.Group("/v1")

httprpc.RegisterHandler(v1, httprpc.GET(handler, "/users"))
// Registers at /api/v1/users

Groups inherit middleware from parents.

Codecs

Codecs handle request/response encoding/decoding. JSON is used by default:

// DefaultCodec: JSON bodies, query param decoding for GET.
// Custom codecs implement DecodeBody/DecodeQuery/Encode/EncodeError.
httprpc.RegisterHandler(r, endpoint, httprpc.WithCodec[Req, Res](customCodec))

Implement the Codec[Req, Res] interface for custom codecs.

For meta-aware handlers:

httprpc.RegisterHandlerM(r, endpoint, httprpc.WithCodecWithMeta[Req, Meta, Res](customCodec))

Error Handling

Use StatusError to return HTTP status codes:

return nil, httprpc.StatusError{Status: http.StatusBadRequest, Err: errors.New("invalid input")}

Decode failures automatically return 400 Bad Request.

TypeScript Client Generation

Generate TypeScript clients from registered endpoints. Path params come from route patterns, and header tags on meta structs become typed headers parameters in the generated client.

Single File
var buf bytes.Buffer
opts := httprpc.TSGenOptions{
    ClientName: "APIClient",
}
if err := r.GenTS(&buf, opts); err != nil {
    log.Fatal(err)
}
Multi-File (by Path Segment)
opts := httprpc.TSGenOptions{
    SkipPathSegments: 1, // Skip /api/v1/ prefix
}
if err := r.GenTSDir("client", opts); err != nil {
    log.Fatal(err)
}

This generates:

  • base.ts: Base client class
  • <module>.ts: Module-specific clients and types
  • index.ts: Main export
go:generate

Create a generator file:

//go:generate go run ./gen
package main

import (
    "log"
    "github.com/behzade/httprpc"
)

func main() {
    r := httprpc.New()
    // Register your endpoints here
    if err := r.GenTSDir("../client", httprpc.TSGenOptions{}); err != nil {
        log.Fatal(err)
    }
}

Then run go generate in the directory containing the comment.

Runtime Generation

Configure the router and invoke generation explicitly (e.g., behind a CLI flag):

r.SetTSClientGenConfig(&httprpc.TSClientGenConfig{
    Dir: "client",
    Options: httprpc.TSGenOptions{
        ClientName: "API",
        SkipPathSegments: 1,
    },
})
if err := r.GenerateTSClient(); err != nil {
    log.Fatal(err)
}

Requirements

  • Go 1.25.4 or later
  • JSON tags on struct fields must be snake_case (e.g., json:"field_name")

Development

This repo uses Devbox to manage the toolchain (Go, golangci-lint, Node, pnpm).

Use devbox shell for an interactive dev environment, or run scripts directly:

  • devbox run lint: Run golangci-lint with repo settings.
  • devbox run fmt: Format Go code via golangci-lint fmt.
  • devbox run test: Run Go tests.
  • devbox run test:bench: Run Go benchmarks.
  • devbox run example:gen: Generate the example client (example, go run . -gen).
  • devbox run example:run: Build the example frontend and run the server.
  • devbox run example:dev: Start the example frontend dev server and run the backend against it.

License

See LICENSE file. README.md

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func PathParam

func PathParam(ctx context.Context, name string) (string, bool)

PathParam returns a path parameter by name.

func PathParams

func PathParams(ctx context.Context) map[string]string

PathParams returns a copy of all path params.

func RegisterHandler

func RegisterHandler[Req, Res any](eg *EndpointGroup, in Endpoint[Req, Res], opts ...RegisterOption[Req, Res])

RegisterHandler registers an endpoint with the endpoint group.

func RegisterHandlerM

func RegisterHandlerM[Req, Meta, Res any](eg *EndpointGroup, in EndpointWithMeta[Req, Meta, Res], opts ...RegisterOptionWithMeta[Req, Meta, Res])

RegisterHandlerM registers an endpoint with typed metadata.

Types

type Codec

type Codec[Req any, Res any] interface {
	DecodeBody(*http.Request) (Req, error)
	DecodeQuery(*http.Request) (Req, error)
	Encode(http.ResponseWriter, Res) error
	EncodeError(http.ResponseWriter, error) error
}

Codec defines the interface for encoding and decoding HTTP requests and responses.

type DefaultCodec

type DefaultCodec[Req any, Res any] struct {
	Status int
}

DefaultCodec implements Codec using JSON for bodies and structured query decoding.

func (DefaultCodec[Req, Res]) Consumes

func (c DefaultCodec[Req, Res]) Consumes() []string

Consumes returns the content types this codec can decode.

func (DefaultCodec[Req, Res]) DecodeBody

func (c DefaultCodec[Req, Res]) DecodeBody(r *http.Request) (Req, error)

DecodeBody decodes the request body into the request type using JSON.

func (DefaultCodec[Req, Res]) DecodeQuery

func (c DefaultCodec[Req, Res]) DecodeQuery(r *http.Request) (Req, error)

DecodeQuery decodes query parameters into the request type.

func (DefaultCodec[Req, Res]) Encode

func (c DefaultCodec[Req, Res]) Encode(w http.ResponseWriter, res Res) error

Encode encodes the response into the HTTP response writer.

func (DefaultCodec[Req, Res]) EncodeError

func (c DefaultCodec[Req, Res]) EncodeError(w http.ResponseWriter, err error) error

EncodeError encodes an error into the HTTP response writer.

func (DefaultCodec[Req, Res]) Produces

func (c DefaultCodec[Req, Res]) Produces() []string

Produces returns the content types this codec can encode.

type Endpoint

type Endpoint[Req, Res any] struct {
	Handler Handler[Req, Res]
	Path    string
	Method  string
}

Endpoint represents an HTTP endpoint with a handler, path, and method.

func DELETE

func DELETE[Req, Res any](handler Handler[Req, Res], path string) Endpoint[Req, Res]

DELETE creates an Endpoint for HTTP DELETE requests.

func GET

func GET[Req, Res any](handler Handler[Req, Res], path string) Endpoint[Req, Res]

GET creates an Endpoint for HTTP GET requests.

func HEAD[Req, Res any](handler Handler[Req, Res], path string) Endpoint[Req, Res]

HEAD creates an Endpoint for HTTP HEAD requests.

func OPTIONS

func OPTIONS[Req, Res any](handler Handler[Req, Res], path string) Endpoint[Req, Res]

OPTIONS creates an Endpoint for HTTP OPTIONS requests.

func PATCH

func PATCH[Req, Res any](handler Handler[Req, Res], path string) Endpoint[Req, Res]

PATCH creates an Endpoint for HTTP PATCH requests.

func POST

func POST[Req, Res any](handler Handler[Req, Res], path string) Endpoint[Req, Res]

POST creates an Endpoint for HTTP POST requests.

func PUT

func PUT[Req, Res any](handler Handler[Req, Res], path string) Endpoint[Req, Res]

PUT creates an Endpoint for HTTP PUT requests.

type EndpointDescription

type EndpointDescription struct {
	Method string
	Path   string

	Req  TypeRef
	Meta TypeRef
	Res  TypeRef

	Consumes []string
	Produces []string
}

EndpointDescription describes an endpoint for TypeScript generation.

type EndpointGroup

type EndpointGroup struct {
	Prefix      string
	Handlers    []*endpoint
	Middlewares []*MiddlewareWithPriority

	Metas []*EndpointMeta
	// contains filtered or unexported fields
}

EndpointGroup groups endpoints with a common prefix and middlewares.

func (*EndpointGroup) Group

func (eg *EndpointGroup) Group(prefix string) *EndpointGroup

Group creates a subgroup with the given prefix.

func (*EndpointGroup) Use

func (eg *EndpointGroup) Use(middleware Middleware, middlewareOpts ...MiddlewareOption)

Use adds a middleware to this group (and its sub-groups).

type EndpointMeta

type EndpointMeta struct {
	Method string
	Path   string

	Req  reflect.Type
	Meta reflect.Type
	Res  reflect.Type

	Consumes []string
	Produces []string
}

EndpointMeta contains metadata about an endpoint.

type EndpointWithMeta

type EndpointWithMeta[Req, Meta, Res any] struct {
	Handler HandlerWithMeta[Req, Meta, Res]
	Path    string
	Method  string
}

EndpointWithMeta represents an HTTP endpoint with typed request metadata.

func DELETEM

func DELETEM[Req, Meta, Res any](handler HandlerWithMeta[Req, Meta, Res], path string) EndpointWithMeta[Req, Meta, Res]

DELETEM creates an EndpointWithMeta for HTTP DELETE requests.

func GETM

func GETM[Req, Meta, Res any](handler HandlerWithMeta[Req, Meta, Res], path string) EndpointWithMeta[Req, Meta, Res]

GETM creates an EndpointWithMeta for HTTP GET requests.

func HEADM

func HEADM[Req, Meta, Res any](handler HandlerWithMeta[Req, Meta, Res], path string) EndpointWithMeta[Req, Meta, Res]

HEADM creates an EndpointWithMeta for HTTP HEAD requests.

func OPTIONSM

func OPTIONSM[Req, Meta, Res any](handler HandlerWithMeta[Req, Meta, Res], path string) EndpointWithMeta[Req, Meta, Res]

OPTIONSM creates an EndpointWithMeta for HTTP OPTIONS requests.

func PATCHM

func PATCHM[Req, Meta, Res any](handler HandlerWithMeta[Req, Meta, Res], path string) EndpointWithMeta[Req, Meta, Res]

PATCHM creates an EndpointWithMeta for HTTP PATCH requests.

func POSTM

func POSTM[Req, Meta, Res any](handler HandlerWithMeta[Req, Meta, Res], path string) EndpointWithMeta[Req, Meta, Res]

POSTM creates an EndpointWithMeta for HTTP POST requests.

func PUTM

func PUTM[Req, Meta, Res any](handler HandlerWithMeta[Req, Meta, Res], path string) EndpointWithMeta[Req, Meta, Res]

PUTM creates an EndpointWithMeta for HTTP PUT requests.

type Handler

type Handler[Req any, Res any] func(ctx context.Context, request Req) (Res, error)

Handler is a simple function type for handling requests.

type HandlerMiddleware

type HandlerMiddleware[Req, Res any] func(next Handler[Req, Res]) Handler[Req, Res]

HandlerMiddleware is a middleware function for typed handlers.

type HandlerWithMeta

type HandlerWithMeta[Req, Meta, Res any] func(ctx context.Context, request Req, meta Meta) (Res, error)

HandlerWithMeta is a handler that receives request metadata decoded from path/header tags.

type HandlerWithMetaMiddleware

type HandlerWithMetaMiddleware[Req, Meta, Res any] func(next HandlerWithMeta[Req, Meta, Res]) HandlerWithMeta[Req, Meta, Res]

HandlerWithMetaMiddleware is a middleware function for typed handlers with metadata.

type Middleware

type Middleware func(next http.Handler) http.Handler

Middleware is a function that wraps an http.Handler.

type MiddlewareOption

type MiddlewareOption interface {
	// contains filtered or unexported methods
}

MiddlewareOption configures middleware options.

func Priority

func Priority(priority int) MiddlewareOption

Priority sets middleware ordering. Higher priority runs earlier (wraps outer). For equal priority, later-registered middleware wraps outer.

type MiddlewareWithPriority

type MiddlewareWithPriority struct {
	Middleware Middleware
	Priority   int
}

MiddlewareWithPriority associates a middleware with a priority level.

type RegisterOption

type RegisterOption[Req, Res any] interface {
	// contains filtered or unexported methods
}

RegisterOption configures options for registering a handler.

func WithCodec

func WithCodec[Req, Res any](codec Codec[Req, Res]) RegisterOption[Req, Res]

WithCodec sets the codec for the handler.

func WithMiddleware

func WithMiddleware[Req, Res any](middleware HandlerMiddleware[Req, Res]) RegisterOption[Req, Res]

WithMiddleware adds a middleware to the handler.

func WithMiddlewares

func WithMiddlewares[Req, Res any](middlewares ...HandlerMiddleware[Req, Res]) RegisterOption[Req, Res]

WithMiddlewares adds multiple middlewares to the handler.

type RegisterOptionWithMeta

type RegisterOptionWithMeta[Req, Meta, Res any] interface {
	// contains filtered or unexported methods
}

RegisterOptionWithMeta configures options for registering a handler with metadata.

func WithCodecWithMeta

func WithCodecWithMeta[Req, Meta, Res any](codec Codec[Req, Res]) RegisterOptionWithMeta[Req, Meta, Res]

WithCodecWithMeta sets the codec for the handler with metadata.

func WithMetaMiddleware

func WithMetaMiddleware[Req, Meta, Res any](middleware HandlerWithMetaMiddleware[Req, Meta, Res]) RegisterOptionWithMeta[Req, Meta, Res]

WithMetaMiddleware adds a middleware to the handler with metadata.

func WithMetaMiddlewares

func WithMetaMiddlewares[Req, Meta, Res any](middlewares ...HandlerWithMetaMiddleware[Req, Meta, Res]) RegisterOptionWithMeta[Req, Meta, Res]

WithMetaMiddlewares adds multiple middlewares to the handler with metadata.

type Router

type Router struct {
	*EndpointGroup
	// contains filtered or unexported fields
}

Router is the main router for handling HTTP requests.

func New

func New() *Router

New creates a new Router.

func (*Router) Describe

func (r *Router) Describe() []EndpointDescription

Describe returns endpoint metadata suitable for generators.

func (*Router) GenTS

func (r *Router) GenTS(w io.Writer, opts TSGenOptions) error

GenTS writes a TypeScript client based on registered endpoint metadata.

Intended usage with go:generate:

//go:generate go run ./cmd/gen

where ./cmd/gen constructs your router and calls router.GenTS(...).

func (*Router) GenTSDir

func (r *Router) GenTSDir(dir string, opts TSGenOptions) error

GenTSDir writes a multi-file TypeScript client into dir, split by path segment. It overwrites the generated files it creates.

func (*Router) GenerateTSClient

func (r *Router) GenerateTSClient() error

GenerateTSClient generates the TypeScript client using the configured TSClientGenConfig. No-op if no config is set. Errors are returned to the caller, and also passed to cfg.OnError if provided.

func (*Router) Handler

func (r *Router) Handler() (http.Handler, error)

Handler builds and returns an http.Handler for the registered endpoints. It returns an error if routes are invalid (e.g., duplicate method+path).

func (*Router) HandlerMust

func (r *Router) HandlerMust() http.Handler

HandlerMust returns the handler or panics if building the handler fails.

func (*Router) RunServer

func (r *Router) RunServer(addr string, opts ...RunServerOption) error

RunServer runs the HTTP server. By default, it enables graceful shutdown with a 30-second timeout. It blocks until the server is shut down.

Options:

  • WithGracefulShutdown(timeout): Enable graceful shutdown (default: enabled with 30s timeout)
  • WithLogger(logger): Custom logger for server events (default: slog.Default())

Example:

// Simple usage with defaults (graceful shutdown enabled)
r.RunServer(":8080")

// Custom timeout
r.RunServer(":8080", httprpc.WithGracefulShutdown(60*time.Second))

// Custom logger
r.RunServer(":8080", httprpc.WithLogger(myLogger))

func (*Router) Server

func (r *Router) Server(addr string, opts ...ServerOption) *http.Server

Server returns a configured http.Server using Router.Handler().

func (*Router) SetFallback

func (r *Router) SetFallback(h http.Handler)

SetFallback sets a handler that is invoked when no registered endpoint matches the request path. Middlewares registered on the root group still apply to the fallback.

func (*Router) SetTSClientGenConfig

func (r *Router) SetTSClientGenConfig(cfg *TSClientGenConfig)

SetTSClientGenConfig sets the configuration for TypeScript client generation.

func (*Router) Use

func (r *Router) Use(middleware Middleware, middlewareOpts ...MiddlewareOption)

Use adds a middleware to the router. Priority -> Higher means earlier execution.

type RunServerOption

type RunServerOption interface {
	// contains filtered or unexported methods
}

RunServerOption configures RunServer behavior.

func WithGracefulShutdown

func WithGracefulShutdown(timeout time.Duration) RunServerOption

WithGracefulShutdown enables graceful shutdown on SIGINT/SIGTERM signals. This is the default behavior. Use this option to explicitly enable it or to customize the shutdown timeout.

func WithLogger

func WithLogger(logger *slog.Logger) RunServerOption

WithLogger sets a custom logger for server lifecycle events. If not provided, slog.Default() is used.

type ServerOption

type ServerOption interface {
	// contains filtered or unexported methods
}

ServerOption configures server options.

func IdleTimeout

func IdleTimeout(d time.Duration) ServerOption

IdleTimeout sets the IdleTimeout for the server.

func MaxHeaderBytes

func MaxHeaderBytes(n int) ServerOption

MaxHeaderBytes sets the MaxHeaderBytes for the server.

func ReadHeaderTimeout

func ReadHeaderTimeout(d time.Duration) ServerOption

ReadHeaderTimeout sets the ReadHeaderTimeout for the server.

func ReadTimeout

func ReadTimeout(d time.Duration) ServerOption

ReadTimeout sets the ReadTimeout for the server.

func WriteTimeout

func WriteTimeout(d time.Duration) ServerOption

WriteTimeout sets the WriteTimeout for the server.

type StatusError

type StatusError struct {
	Status int
	Err    error
}

StatusError marks an error with an HTTP status code for encoding. It can be returned by handlers or used internally (e.g. decode failures).

func (StatusError) Error

func (e StatusError) Error() string

func (StatusError) Unwrap

func (e StatusError) Unwrap() error

type TSClientGenConfig

type TSClientGenConfig struct {
	// Dir is the output directory for generated TypeScript.
	// If empty, generation is disabled.
	Dir string

	// Options are passed through to GenTSDir.
	Options TSGenOptions

	// OnError is called if GenerateTSClient encounters an error.
	// If nil, errors are only returned to the caller.
	OnError func(error)
}

TSClientGenConfig configures TypeScript client generation.

type TSGenOptions

type TSGenOptions struct {
	PackageName string
	ClientName  string
	// SkipPathSegments skips leading path segments when choosing a module file.
	// Example: for "/v1/users/list" and SkipPathSegments=1, the module is "users".
	SkipPathSegments int
}

TSGenOptions configures TypeScript generation.

type TypeRef

type TypeRef struct {
	String  string
	Name    string
	PkgPath string
}

TypeRef represents a reference to a Go type.

Directories

Path Synopsis
Package middleware provides HTTP middleware utilities for the httprpc framework.
Package middleware provides HTTP middleware utilities for the httprpc framework.

Jump to

Keyboard shortcuts

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