SRouter leverages Go 1.18+ generics to provide type-safe handling of request and response data. This eliminates the need for manual type assertions and reduces boilerplate code.
Generic routes are defined using the RouteConfig[T, U] struct, where T is the request type and U is the response type. They require a Codec for marshaling/unmarshaling and a GenericHandler.
// Define request and response types
type CreateUserReq struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResp struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// Define a generic handler function
// It takes the http.Request and the decoded request object (type T)
// It returns the response object (type U) and an error
func CreateUserHandler(r *http.Request, req CreateUserReq) (CreateUserResp, error) {
// Access request context if needed, e.g., for UserID, Transaction, etc.
// userID, ok := scontext.GetUserIDFromRequest[string, string](r) // Use scontext, replace types as needed
// txInterface, txOK := scontext.GetTransactionFromRequest[string, string](r) // Use scontext
// if txOK { gormTx := txInterface.GetDB() /* use gormTx */ }
fmt.Printf("Received request to create user: Name=%s, Email=%s\n", req.Name, req.Email)
// In a real application, you would interact with a database or service
// If an error occurs (e.g., validation, database error), return it:
// if req.Name == "" {
// return CreateUserResp{}, router.NewHTTPError(http.StatusBadRequest, "Name cannot be empty")
// }
// Simulate successful creation
createdUser := CreateUserResp{
ID: "user-" + uuid.NewString(), // Example ID
Name: req.Name,
Email: req.Email,
}
return createdUser, nil // Return the response object and nil error on success
}
// Define the route configuration
createUserRoute := router.RouteConfig[CreateUserReq, CreateUserResp]{
Path: "/users",
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.AuthRequired), // Example: Requires authentication
Codec: codec.NewJSONCodec[CreateUserReq, CreateUserResp](), // Specify the codec
Handler: CreateUserHandler, // Assign the generic handler
// Optional overrides for timeout, body size, or rate limit
Overrides: common.RouteOverrides{
// Timeout: 3 * time.Second,
// MaxBodySize: 2 << 20, // 2 MB
// RateLimit: &common.RateLimitConfig[any, any]{...},
},
Sanitizer: func(req CreateUserReq) (CreateUserReq, error) { // Optional: Sanitize data after decoding
if req.Name == "invalid" {
return CreateUserReq{}, router.NewHTTPError(http.StatusBadRequest, "Invalid name provided")
}
// Example: Trim spaces
req.Name = strings.TrimSpace(req.Name)
return req, nil // Return the modified request (or original if no changes) and nil error
},
}The preferred and recommended way to register generic routes is declaratively within a SubRouterConfig using the NewGenericRouteDefinition helper function. This ensures that path prefixes, middleware, and configuration overrides (timeout, max body size, rate limit) are correctly applied.
// Define the route configuration (as shown previously)
createUserRoute := router.RouteConfig[CreateUserReq, CreateUserResp]{ /* ... */ }
// Define the SubRouterConfig
apiV1SubRouter := router.SubRouterConfig{
PathPrefix: "/api/v1",
// Middlewares specific to this sub-router can go here
Routes: []router.RouteDefinition{
// ... other routes (RouteConfigBase or other NewGenericRouteDefinition calls) ...
// Use NewGenericRouteDefinition to wrap the generic RouteConfig.
// The last two type parameters (string, string) must match the
// UserIDType and UserObjectType used in NewRouter[UserIDType, UserObjectType].
router.NewGenericRouteDefinition[CreateUserReq, CreateUserResp, string, string](createUserRoute),
},
// Optional overrides for all routes in this sub-router
Overrides: common.RouteOverrides{
// Timeout: 5 * time.Second,
// MaxBodySize: 4 << 20,
// RateLimit: &common.RateLimitConfig[any, any]{...},
},
}
// This SubRouterConfig is then included in the main RouterConfig.SubRouters slice
// passed to router.NewRouter.
routerConfig := router.RouterConfig{
// ... Logger, GlobalTimeout, etc. ...
SubRouters: []router.SubRouterConfig{
apiV1SubRouter,
// Potentially other sub-routers (e.g., for root path: { PathPrefix: "", Routes: [...] })
},
// ...
}
// Create the router
// r := router.NewRouter[string, string](routerConfig, authFunc, userIDFunc)Note on Direct Registration: While a router.RegisterGenericRoute function exists, it's primarily used internally by NewGenericRouteDefinition. Direct use is discouraged as it bypasses the sub-router configuration logic (path prefixing, middleware application, override calculation) and requires manual calculation and passing of effective settings, which can be error-prone. Always prefer the declarative approach using NewGenericRouteDefinition within SubRouterConfig.
RouteConfig[T, U]: Defines the configuration for a generic route, including path, methods, auth level, codec, handler, sanitizer, and overrides.GenericHandler[T, U]: The function signaturefunc(*http.Request, T) (U, error). It receives thehttp.Request(for accessing context, headers, etc.) and the potentially sanitized decoded request objectT. It returns the response objectUand anerror. If the error is non-nil, SRouter handles sending the appropriate HTTP error response (usingrouter.HTTPErrorfor specific status codes).Sanitizer func(T) (T, error): An optional function that runs after the request dataTis successfully decoded by theCodecbut before theGenericHandleris called. It receives the decoded data (T) and can return a modified version of it (T). If it returns a non-nil error, the request processing stops, and a400 Bad Request(or the error specified if it's anHTTPError) is returned. If it returns the modified (or original) data and a nil error, that data is passed to theGenericHandler.Codec[T, U]: An interface responsible for decoding the request (T) and encoding the response (U). See Custom Codecs.NewGenericRouteDefinition: The recommended helper function used withinSubRouterConfig.Routesto wrap aRouteConfig[T, U]for declarative registration, ensuring proper application of sub-router settings.RegisterGenericRoute: An internal function called byNewGenericRouteDefinition. Direct use is discouraged.
SRouter's generic routes offer flexibility in how the request data (T in RouteConfig[T, U]) is retrieved and decoded. By default, it reads from the request body, but you can configure it to read from query or path parameters, especially useful for GET requests or when request bodies are restricted.
This is controlled by the SourceType and SourceKey fields in the RouteConfig[T, U] struct.
SRouter defines constants for the available source types in the router package:
-
router.Body(Default):- Retrieves data directly from the
http.Request.Body. SourceKeyis ignored.- The configured
Codec'sDecodemethod is used. - Example: Standard POST/PUT requests with JSON/Proto payloads.
router.RouteConfig[MyRequest, MyResponse]{ // ... Path, Methods, Handler ... Codec: codec.NewJSONCodec[MyRequest, MyResponse](), // SourceType defaults to Body if omitted }
- Retrieves data directly from the
-
router.Base64QueryParameter:- Retrieves data from a Base64-encoded string in a query parameter.
SourceKeyspecifies the name of the query parameter (e.g.,datafor?data=...).- The value is Base64-decoded, and the resulting bytes are passed to the
Codec'sDecodeBytesmethod. - Example: Sending complex data via GET requests where the data is encoded to fit in the URL.
router.RouteConfig[MyRequest, MyResponse]{ Path: "/data/from/query", Methods: []router.HttpMethod{router.MethodGet}, Handler: MyHandler, Codec: codec.NewJSONCodec[MyRequest, MyResponse](), // Codec still needed for DecodeBytes SourceType: router.Base64QueryParameter, SourceKey: "payload", // Expects URL like /data/from/query?payload=BASE64STRING }
-
router.Base62QueryParameter:- Similar to
Base64QueryParameter, but uses Base62 encoding. Base62 is URL-safe without padding characters, potentially producing shorter strings than Base64. - Retrieves data from a Base62-encoded string in a query parameter specified by
SourceKey. - The value is Base62-decoded, and the bytes are passed to the
Codec'sDecodeBytesmethod.
router.RouteConfig[MyRequest, MyResponse]{ // ... Path, Methods, Handler, Codec ... SourceType: router.Base62QueryParameter, SourceKey: "q", // Expects URL like /path?q=BASE62STRING }
- Similar to
-
router.Base64PathParameter:- Retrieves data from a Base64-encoded string in a named path parameter.
- The route's
Pathmust include a corresponding named parameter (e.g.,/:data). SourceKeyspecifies the name of the path parameter (e.g.,data).- If
SourceKeyis empty, the first path parameter in the request URL is used. - The parameter value is Base64-decoded, and the bytes are passed to the
Codec'sDecodeBytesmethod.
router.RouteConfig[MyRequest, MyResponse]{ Path: "/data/from/path/:payload", // Define path parameter Methods: []router.HttpMethod{router.MethodGet}, Handler: MyHandler, Codec: codec.NewJSONCodec[MyRequest, MyResponse](), SourceType: router.Base64PathParameter, SourceKey: "payload", // Matches the :payload name in the Path }
-
router.Base62PathParameter:- Similar to
Base64PathParameter, but uses Base62 encoding. - Retrieves data from a Base62-encoded string in a named path parameter specified by
SourceKey. - If
SourceKeyis empty, the first path parameter in the request URL is used. - The parameter value is Base62-decoded, and the bytes are passed to the
Codec'sDecodeBytesmethod.
router.RouteConfig[MyRequest, MyResponse]{ Path: "/data/b62/:p", // Define path parameter Methods: []router.HttpMethod{router.MethodGet}, Handler: MyHandler, Codec: codec.NewJSONCodec[MyRequest, MyResponse](), SourceType: router.Base62PathParameter, SourceKey: "p", // Matches the :p name in the Path }
- Similar to
-
router.Empty:- No request decoding is performed. The handler receives the zero value of the request type.
- Useful for endpoints that do not accept input but still use generic handlers.
Even when using query or path parameter source types, a Codec is still required in the RouteConfig. This is because the router needs the codec's DecodeBytes method to unmarshal the decoded byte slice ([]byte) into the target request type T.
// Codec interface likely includes:
type Codec[T any, U any] interface {
// ... Decode(r *http.Request) (T, error) ...
DecodeBytes(data []byte) (T, error) // Used by non-Body source types
// ... Encode(w http.ResponseWriter, resp U) error ...
// ... NewRequest() T ...
}Ensure your chosen codec implements DecodeBytes correctly for the data format you expect (e.g., JSON, Proto).
See the examples/source-types directory for a runnable example demonstrating different source types.
Using generic routes significantly improves type safety and developer experience when dealing with structured request and response data.