HAP
This package implements a declarative HTTP framework for rapid API service development. Key features include:
Code-as-Specification: API definitions serve as documentation specifications. The framework automatically generates API documentation directly from code-based endpoint declarations. 1.
Layered Processing Isolation: Separation of concerns between parameter validation and business logic, with framework-enforced validation pipeline definition and execution. 1.
Composable Processing Pipelines: Both parameter handling and business logic support multi-stage function composition, with explicit streamlined processing rules. 1.
Standard Library Foundation: Native integration with Go’s net/http package allows direct use of core primitives like Request an…
HAP
This package implements a declarative HTTP framework for rapid API service development. Key features include:
Code-as-Specification: API definitions serve as documentation specifications. The framework automatically generates API documentation directly from code-based endpoint declarations. 1.
Layered Processing Isolation: Separation of concerns between parameter validation and business logic, with framework-enforced validation pipeline definition and execution. 1.
Composable Processing Pipelines: Both parameter handling and business logic support multi-stage function composition, with explicit streamlined processing rules. 1.
Standard Library Foundation: Native integration with Go’s net/http package allows direct use of core primitives like Request and ResponseWriter, ensuring zero learning curve for developers familiar with standard HTTP handling patterns.
Structure and Conventions
HAP is not a full-stack framework, nor does it provide functionalities like ORM. Instead, it is a minimalist and opinionated framework. It divides a complete HTTP request handling process into three distinct phases: parameter processing, business logic processing, and response generation.
Parameter Processing
The parameter processing phase is responsible for extracting parameters from HTTP requests (parameter collection), converting them into the format required by the business logic, and performing necessary validation (parameter validation).
Parameter Collection
The parameters of an HTTP request can be sourced from:
- URL Path Parameters: For example, if a Handler’s Pattern is
/user/{id}, thenidis a URL path parameter. - URL Query Parameters: Used in GET requests, e.g.,
idandnamein/user?id=123&name=foo. - HTTP Request Body: Used in POST requests. The framework supports parsing MIME types including
application/json,application/x-www-form-urlencoded, andmultipart/form-data. - HTTP Request Cookies: For security reasons, the framework disables parameter transmission via cookies by default. This can be enabled using the AllowCookie function.
- HTTP Request Headers: The framework extracts headers starting with X- into the parameter set. For example,
X-User-ID: 123is parsed asuser-id=123(Note: Parameter names are converted to lowercase for compatibility) .
Parameter Collection Rules:
-
Priority Order: If duplicate parameter names exist across sources, priority is determined in the order listed above. Path parameters have the highest priority, followed by query parameters, then the request body.
-
HTTP Method Flexibility: Regardless of the HTTP method (e.g., GET, POST), parameters can be placed in any allowed locations. For example, POST requests may include parameters in both the URL query string and the request body, following the defined priority order . This design ensures unified parameter handling across all methods while maintaining backward compatibility with standard practices.
-
JSON Request Body Handling: For MIME type application/json, the framework parses the JSON data into key/value pairs. Keys must be string, and values can be either string or []string. To bypass JSON parsing (e.g., for non-compliant JSON), either:
-
Omit the Content-Type header or set to a non-JSON type
-
Use the WithRawJSONBody function to disable JSON body parsing
Parameter Validation
HAP defines a chainable parameter processing workflow where one or more validation functions can be specified during parameter definition. For example:
hap.Register(api.New("/api/user/{id}").WithHandlers(
handler.GET().WithParams(
param.New[int]("id").WithCheckers(
param.ValueBetween(1, 1000).WithHelp("User ID must be between 1-1000"),
).WithHelp("User ID")
).WithActions(handleUserGet).WithHelp("Get user details")
))
In the WithCheckers function, multiple validators can be added. These validators execute sequentially. If any validator fails, the framework returns 400 Bad Request and terminates processing. Note: This error might be returned as HTTP status code 200, see the Response section for details.
The framework provides common validators, including:
ValueBetween: Value must be within a specified rangeValueLessThan: Value must be less than a specified valueValueGreaterThan: Value must be greater than a specified valueTextIsOneOf: Value must match one of the specified stringsTextHasPrefix: Value must start with a specified stringTextConsistsOf: Value must contain only specified characters- Many others (see API documentation).
Custom validators can be created using NewChecker. Validators can not only verify compliance but also transform parameters. For example:
- Convert validated strings to uppercase
- Map input values to predefined indexes
Business Logic Processing
The business logic processing phase forms the core of the framework, defined by the WithActions function which accepts one or more Action functions. These functions execute sequentially, and the next function’s execution depends on the previous function’s return value. The interface of an Action function is defined as:
func(*arg.Args, http.ResponseWriter, *http.Request) any
The first parameter is a pointer to the aggregated parameters from previous stage. It enables in-place modifications that propagate through the action chain.
The return value is of type any, but the permitted types and their meanings are limited to:
- nil: Indicates the response has been completely handled within the Action (e.g., image/file data sent directly through ResponseWriter). The framework terminates processing immediately without additional returns.
- ReplySpec: Represents the final processing result to be returned to the client.
- error: Triggers a 500 Internal Server Error response with error details.
- *arg.Args: Signals continuation of processing chain with modified parameters.
Note: If Action returns data other than the above types, the framework will panic. Both returned errors and panics will ultimately be wrapped into ReplySpec structures. The final response format (JSON or RAW) depends on the current “response mode” configuration (see Response section).
Response
The framework provides two return modes: API mode and RAW mode. It is important to note that the return modes discussed here only apply to cases where the Action function returns a ReplySpec (of course, if an error is returned or a panic is caught, it will also be converted into a ReplySpec). If the Action function returns nil, it means the response content is actually provided by the Action function itself, and the framework has no control over it.
API Mode
In this mode, regardless of the result of the Action function, the framework will always return an HTTP status code of 200 and convert the response to JSON format. For example, if a validator returns an error during the parameter processing phase, the framework will return a 400 error code embedded in the JSON body (not as the HTTP status code).
For instance, the following code:
return hap.Reply(http.StatusBadRequest).WithMesg("Missing required parameter 't'")
will produce:
HTTP/1.1 200 OK
Content-Type: application/json
...
{
"code": 400,
"mesg": "Missing required parameter 't'"
}
RAW Mode
If th.e Action function returns a RawReply instead of a Reply, for example:
return hap.RawReply(http.StatusOK).WithData(data)
the framework will not return JSON but directly output the raw data:
HTTP/1.1 200 OK
Content-Type: application/octet-stream
...
[data]
Special Cases
-
Errors or Panics: If the Action function returns an error or a panic is caught, the framework defaults to API mode for the response format. This behavior can be switched to RAW mode by using WithRawReply when defining the handler.
-
Forced RAW Mode: Regardless of the framework’s global return mode setting, the following scenarios always enforce RAW mode:
-
Non-existent API endpoints: The framework directly returns a raw HTTP response with status code 404 Not Found.
-
Unhandled HTTP methods: The framework returns a raw HTTP response with status code 405 Method Not Allowed.
Implementation Examples
i18n Support
HAP handles i18n by using Go’s standard internationalization library (golang.org/x/text). The framework processes Accept-Language headers following RFC 7231 specifications while providing high-level abstractions. The key implementation lies in the following code snippet from ServeHTTP:
lang := internal.MatchPreferredLanguage(r)
a.i18nOut = message.NewPrinter(lang)
w.Header().Set("Content-Language", lang.String())
The internal MatchPreferredLanguage function is the core of this mechanism.
Under the internal package, the internal/lang_cn.go file demonstrates how to add Simplified Chinese translations for framework-level strings. Note that the language is only “registered” but not “activated”. To actually use that language use the LoadLanguage function. This function also accepts a map containing extra translations. To add more languages, call LoadLanguage multiple times, once for each language. Note:
- It is not mandatory to register a language before using LoadLanguage. As a matter of fact, the translations shown in
internal/lang_cn.gocan also be passed to LoadLanguage via theextraparameter. - You may also use go’s [message.SetString] function to register individual translations directly.
Distributed Tracing Integration
HAP integrates with the W3C TraceContext compliant Traceparent identifier to facilitate distributed tracing systems.
When requests are routed to the framework via Go’s http.ServeMux, ServeHTTP automatically generates a TraceContext object in the context. This object persists throughout the entire request lifecycle, recording all information from request initiation to response completion.
Before returning the response, the framework invokes function registered via WithPostResponseTrace. This function can export the TraceContext data to third-party logging systems. Complete implementation example:
func ValidateToken(a *arg.Args, w http.ResponseWriter, r *http.Request) any {
t := arg.Get[string](a, "t")
user, err := CheckToken(t)
if err != nil {
return hap.Reply(http.StatusUnauthorized).WithHelp(err.Error())
}
if tc := hap.GetTraceContext(r); tc != nil {
tc.Index("user_login", user.Login)
}
return a
}
func handleUserGet(a *arg.Args, w http.ResponseWriter, r *http.Request) any {
// Business logic implementation
}
func main() {
// ...
// Register API endpoint
hap.Register(api.New("/api/users").WithHandlers(
handler.GET().WithParams(
param.New[string]("t").IsRequired().WithHelp("Access token"),
).WithActions(
ValidateToken,
handleUserGet
).WithHelp("Retrieve user information"),
))
// Configure tracing export
hap.WithPostResponseTrace(func(r *http.Request) {
tc := hap.GetTraceContext(r)
if tc == nil {
return
}
tracer := otel.Tracer("hap.service")
traceID, _ := otelTrace.TraceIDFromHex(tc.TraceId)
spanID, _ := otelTrace.SpanIDFromHex(tc.SpanId)
var parentSpanID otelTrace.SpanID
if tc.ParentSpanId != "" {
parentSpanID, _ = otelTrace.SpanIDFromHex(tc.ParentSpanId)
}
_, span := tracer.Start(
context.Background(),
tc.Route,
otelTrace.WithSpanKind(otelTrace.SpanKindServer),
otelTrace.WithTimestamp(time.Unix(0, tc.StartTime)),
otelTrace.WithLinks(otelTrace.LinkFromContext(ctx)),
otelTrace.WithTraceID(traceID),
otelTrace.WithSpanID(spanID),
)
// Set standard attributes
span.SetAttributes(
attribute.String("service.name", tc.Service.Name),
attribute.String("service.version", tc.Service.Version),
attribute.String("service.instance.id", tc.Service.Address),
attribute.String("deployment.environment", tc.Service.Environ),
attribute.String("http.route", tc.Route),
attribute.Int("http.status_code", tc.Reply.StatusCode()),
)
// ... ...
for k, v := range tc.Indexes {
span.SetAttributes(attribute.String(k, v))
}
for k, v := range tc.Props {
span.SetAttributes(attribute.String(k, v))
}
span.End(otelTrace.WithTimestamp(time.Unix(0, tc.EndTime)))
})
// ...
}
Self-Documenting System
One of HAP’s core philosophies is self-documenting code. As demonstrated in previous sections, API endpoints automatically generate documentation through declarative helper functions (primarily WithHelp).
To enable API documentation generation, the framework provides the ListAPIs function:
var nr *hap.NetRange
if debugMode { // Default: localhost-only access. Debug mode opens to all IPs
_, ns, _ := net.ParseCIDR("0.0.0.0/0")
nr = hap.NewNetRange(*ns)
}
hap.Register(api.ListAPIs("/api", nr))
This registers an endpoint that lists all registered API information for documentation purposes. When registering the endpoint:
- The NetRange object controls IP ranges that are allowed to get the list of APIs
- In debug mode (0.0.0.0/0 CIDR) allows full access
- In production mode (nil NetRange) only permits localhost access
The output of ListAPIs is a JSON object that can be read by documentation generation tools (please run the example program to view the specific output format).
Mock Data Service
When developing an API service, you can define the API specification and provide mock data to help frontend developers quickly build pages. HAP provides a mechanism to automatically return mock data through WithReplySpec. Below is an example:
hap.Register(api.New("/api/users/{id}").WithHandlers(
handler.GET().WithParams(
param.New[string]("t").IsRequired().WithHelp("Access token"),
param.New[int]("id").IsRequired().WithHelp("User ID"),
).WithReplySpec(
hap.Reply(http.StatusOK).WithData(map[string]any{
"id": 1,
"name": "John Doe",
})),
hap.Reply(http.StatusNotFound).WithHelp("User does not exist"),
).WithHelp("Get user information"),
)
Mechanism
- If the API does not define specific business logic (i.e., it does not use WithActions), the framework will directly return the first mock data from ReplySpec.
- If the API defines a business function but returns 501 Not Implemented, the framework will also return mock data.
Response Format
The returned mock data includes a prompt in the mesg field of the JSON response to indicate that the data is mock, helping frontend developers distinguish it from real data. For example:
{
"code": 200,
"data": {
"id": 1,
"name": "John Doe"
},
"mesg": "Mock data. Backend not implemented."
}