htr(HTTP to RPC converter)

A convenient bridge between HTTP requests and gRPC calls, simplifying the process of converting HTTP endpoints to RPC service calls.

The htr module provides a convenient bridge between HTTP requests and gRPC calls, simplifying the process of converting HTTP endpoints to RPC service calls with automatic request parsing, validation, and response handling.

Features

  • Generic type support for type-safe request/response handling
  • Automatic request parsing from HTTP/Gin contexts to structured types
  • Built-in validation using go-playground/validator
  • Error handling integration with errorx module
  • Flexible middleware hooks with BindAfter and RespAfter options
  • Support for both Gin and standard HTTP handlers
  • Structured JSON responses with consistent error formatting

Basic Usage with Gin

 1package main
 2
 3import (
 4    "context"
 5    
 6    "github.com/gin-gonic/gin"
 7    "google.golang.org/grpc"
 8    
 9    "github.com/crazyfrankie/frx/htr"
10    "your-project/pb" // Your protobuf generated code
11)
12
13type CreateUserRequest struct {
14    Name  string `json:"name" binding:"required"`
15    Email string `json:"email" binding:"required,email"`
16    Age   int    `json:"age" binding:"min=1,max=120"`
17}
18
19type CreateUserResponse struct {
20    ID   int64  `json:"id"`
21    Name string `json:"name"`
22}
23
24func CreateUserHandler(c *gin.Context) {
25    // Direct HTTP to RPC conversion
26    htr.Call(c, pb.NewUserServiceClient(grpcConn).CreateUser, grpcClient)
27}
28
29func main() {
30    r := gin.Default()
31    
32    // Setup gRPC connection
33    grpcConn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
34    if err != nil {
35        panic(err)
36    }
37    defer grpcConn.Close()
38    
39    grpcClient := pb.NewUserServiceClient(grpcConn)
40    
41    r.POST("/users", func(c *gin.Context) {
42        htr.Call[CreateUserRequest, CreateUserResponse](
43            c,
44            grpcClient.CreateUser,
45            grpcClient,
46        )
47    })
48    
49    r.Run(":8080")
50}

Advanced Usage with Options

 1func CreateUserWithValidation(c *gin.Context) {
 2    htr.Call[CreateUserRequest, CreateUserResponse](
 3        c,
 4        grpcClient.CreateUser,
 5        grpcClient,
 6        &htr.Option[CreateUserRequest, CreateUserResponse]{
 7            // Custom validation after request binding
 8            BindAfter: func(req *CreateUserRequest) error {
 9                if req.Age < 18 {
10                    return errors.New("user must be at least 18 years old")
11                }
12                
13                // Additional business logic validation
14                if strings.Contains(req.Email, "blocked-domain.com") {
15                    return errors.New("email domain not allowed")
16                }
17                
18                return nil
19            },
20            
21            // Process response before sending to client
22            RespAfter: func(resp *CreateUserResponse) error {
23                // Log successful user creation
24                logs.Infof("User created successfully: ID=%d, Name=%s", 
25                    resp.ID, resp.Name)
26                
27                // Could modify response or trigger side effects
28                return nil
29            },
30        },
31    )
32}

Standard HTTP Handler Usage

 1func CreateUserHTTPHandler(w http.ResponseWriter, r *http.Request) {
 2    htr.CallHttp[CreateUserRequest, CreateUserResponse](
 3        w, r,
 4        grpcClient.CreateUser,
 5        grpcClient,
 6        &htr.Option[CreateUserRequest, CreateUserResponse]{
 7            BindAfter: func(req *CreateUserRequest) error {
 8                // Custom validation logic
 9                return validateUserRequest(req)
10            },
11        },
12    )
13}
14
15func main() {
16    http.HandleFunc("/users", CreateUserHTTPHandler)
17    http.ListenAndServe(":8080", nil)
18}

Error Handling

The htr module integrates with the errorx module for consistent error handling:

 1// Custom error codes (using errorx/gen or manual registration)
 2const (
 3    ErrUserAlreadyExists = 1001001
 4    ErrInvalidAge       = 1001002
 5)
 6
 7func CreateUserWithErrorHandling(c *gin.Context) {
 8    htr.Call[CreateUserRequest, CreateUserResponse](
 9        c,
10        grpcClient.CreateUser,
11        grpcClient,
12        &htr.Option[CreateUserRequest, CreateUserResponse]{
13            BindAfter: func(req *CreateUserRequest) error {
14                // Check if user already exists
15                if userExists(req.Email) {
16                    return errorx.New(ErrUserAlreadyExists,
17                        errorx.KV("email", req.Email))
18                }
19                
20                if req.Age < 18 {
21                    return errorx.New(ErrInvalidAge,
22                        errorx.KV("provided_age", req.Age),
23                        errorx.KV("minimum_age", 18))
24                }
25                
26                return nil
27            },
28        },
29    )
30}

Response Format

The htr module provides consistent JSON response formatting:

Success Response:

1{
2    "code": 0,
3    "message": "",
4    "data": {
5        "id": 12345,
6        "name": "John Doe"
7    }
8}

Error Response:

1{
2    "code": 1001001,
3    "message": "user already exists",
4    "data": null
5}

Multiple Validation Steps

 1func ComplexUserHandler(c *gin.Context) {
 2    htr.Call[CreateUserRequest, CreateUserResponse](
 3        c,
 4        grpcClient.CreateUser,
 5        grpcClient,
 6        &htr.Option[CreateUserRequest, CreateUserResponse]{
 7            BindAfter: func(req *CreateUserRequest) error {
 8                // Step 1: Business logic validation
 9                if err := validateBusinessRules(req); err != nil {
10                    return err
11                }
12                
13                // Step 2: External service validation
14                if err := validateWithExternalService(req); err != nil {
15                    return err
16                }
17                
18                // Step 3: Database constraints
19                if err := validateDatabaseConstraints(req); err != nil {
20                    return err
21                }
22                
23                return nil
24            },
25            
26            RespAfter: func(resp *CreateUserResponse) error {
27                // Send welcome email
28                go sendWelcomeEmail(resp.ID)
29                
30                // Update analytics
31                go updateUserCreationMetrics()
32                
33                return nil
34            },
35        },
36    )
37}

Integration with Context Caching

 1func UserHandlerWithCaching(c *gin.Context) {
 2    htr.Call[GetUserRequest, GetUserResponse](
 3        c,
 4        grpcClient.GetUser,
 5        grpcClient,
 6        &htr.Option[GetUserRequest, GetUserResponse]{
 7            BindAfter: func(req *GetUserRequest) error {
 8                // Cache user permissions for this request
 9                permissions, err := getUserPermissions(req.UserID)
10                if err != nil {
11                    return err
12                }
13                
14                ctxcache.Store(c.Request.Context(), "user_permissions", permissions)
15                return nil
16            },
17            
18            RespAfter: func(resp *GetUserResponse) error {
19                // Use cached permissions for response filtering
20                if permissions, ok := ctxcache.Get[[]string](c.Request.Context(), "user_permissions"); ok {
21                    resp = filterResponseByPermissions(resp, permissions)
22                }
23                return nil
24            },
25        },
26    )
27}