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}