jsendx

jsendx implements an extended JSend response envelope carrying program identity and timestamps, with ready-made handlers for the standard operational routes.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/httputil/jsendx"

One response shape for every endpoint in a service, whether it succeeded, failed, or panicked.

What the envelope adds to JSend

JSend is a minimal convention: a status of success, fail or error, and a data payload. It says nothing about anything else.

Response extends it with fields that answer operational questions from the response body alone:

  • program name, version and release
  • an RFC3339 datetime and a Unix-nanosecond timestamp
  • the HTTP code and message, alongside the JSend status projection
  • the payload itself

The version and release fields are the ones that repay the extra bytes. When a client reports a bad response, the response they captured says which build produced it. Without that, reconciling a report against a deployment timeline means correlating timestamps against a deploy log, across however many replicas were running.

The dual timestamp is deliberate: RFC3339 for a human reading a captured payload, nanoseconds for a machine sorting or diffing. testutil’s ReplaceUnixTimestamp matches that 19-digit field specifically, so a whole response body can be compared against a golden string in a test.

Wrap and send

resp := jsendx.Wrap(http.StatusOK, appInfo, data)

Wrap builds the envelope. JSXResp carries the program identity so handlers do not each have to, and sends wrapped JSON through httputil, inheriting its full-buffer encoding and the 500 fallback on a marshal failure.

AppInfo may be nil, and the envelope still forms; the identity fields are simply absent rather than the call failing.

The handlers you would otherwise write seven times

JSXResp supplies default handlers for the routes every service ends up with:

HandlerPurpose
not-foundrouter fallback for an unmatched path
method-not-allowedrouter fallback for a wrong method
panicrecovery handler
indexroute listing
pingliveness
statusreadiness
public-IPthe address the service sees itself as having

The two router fallbacks are the ones that matter for contract consistency. A service can return a correct envelope from every handler it wrote and still answer a 404 with the router’s plain-text default, which is the response a misconfigured client is most likely to receive and least likely to parse.

The panic handler is the same argument at the other end: an unrecovered panic produces an empty 500 with no envelope and no trace ID.

HealthCheckResultWriter adapts healthcheck output into the same envelope, so a probe result does not arrive in a different shape from everything else.

Ping against status

Both exist because they answer different questions. Ping reports that the process is alive and serving, with no dependency checks, so a liveness probe cannot restart a healthy service because its database is briefly unreachable. Status runs the registered health checks and reports whether the service can actually do work, for a readiness probe.

Wiring both to the same handler is a common way to build a restart loop during a dependency outage.

Example

resp := jsendx.Wrap(http.StatusOK, &jsendx.AppInfo{
    ProgramName:    "example",
    ProgramVersion: "1.0.0",
}, map[string]string{"hello": "world"})

fmt.Println(resp.Status)

Full source is in example_jsendx_test.go. More runnable examples are on pkg.go.dev.

Dependencies

Reaches github.com/julienschmidt/httprouter through httputil, plus healthcheck.