SSE & Streaming¶
See also:
api/reston pkg.go.dev ·adapters/templon pkg.go.devRunnable demos:
examples/adapters-sse·examples/adapters-streaming-sse-templ
Server-Sent Events (SSE)¶
rest.NewSSERoute[Req, Event] registers a typed SSE route — always GET — with an event codec that validates every value before it is serialised to data: ...\n\n.
import (
nethttp "github.com/DaniDeer/go-codex/adapters/nethttp"
"github.com/DaniDeer/go-codex/api/rest"
"github.com/DaniDeer/go-codex/codex"
"github.com/DaniDeer/go-codex/validate"
)
sensorIDCodec := codex.String().Refine(validate.NonEmptyString)
// Declare the SSE route as a value.
sensorRoute, _ := rest.NewSSERoute[struct{}, SensorReading](
"/sensors/{id}/readings",
codex.Empty, sensorReadingCodec,
rest.RouteMeta{OperationID: "streamSensor"},
rest.PathParam{Name: "id", Description: "Sensor ID"}.WithCodec(sensorIDCodec),
).Register(b)
// Wire onto net/http.
nethttp.RegisterSSE(mux, sensorRoute,
func(ctx context.Context, _ struct{}, send func(SensorReading) error) error {
r, _ := nethttp.RequestFromContext(ctx)
sensorID := r.PathValue("id")
for {
select {
case <-ctx.Done():
return nil // client disconnected — context cancelled
default:
}
reading := svc.Read(sensorID)
if err := send(reading); err != nil {
return err // codec rejected the value — nothing written to stream
}
time.Sleep(time.Second)
}
}, nethttp.Options{Observer: obs})
Key properties:
- send(event) validates via the event codec, encodes to JSON, writes data: <json>\n\n, and flushes. If the codec rejects the event, send returns an error without writing anything — the stream remains clean.
- ctx.Done() signals client disconnects; handlers should return nil on context cancellation.
- sensorRoute.BuildPath(vars) validates path variables before assembling the URL — same contract as RouteHandle.BuildPath.
- The route appears in the OpenAPI spec as GET /sensors/{id}/readings with Content-Type: text/event-stream.
- Works identically with chiadapter.SSEHandler / chiadapter.RegisterSSE; use chi.URLParam(r, "id") for path vars.
- The stats observer receives RecordValidationError("response", constraint, "event") for each rejected event — use this to count codec validation failures per event type.
- The stats observer receives RecordValidationError("response", constraint, "event") for each rejected event.
Chunked streaming responses¶
For routes that stream a response body (not SSE), use format.NewStreamed:
import (
adapttempl "github.com/DaniDeer/go-codex/adapters/templ"
"github.com/DaniDeer/go-codex/format"
)
// Chunked streaming HTML page (validates props before committing headers)
dashRoute, _ := rest.NewRoute[struct{}, DashboardProps]("GET", "/dashboard",
codex.Empty, dashPropsCodec, rest.RouteMeta{},
).Register(b)
dashRoute = dashRoute.WithFormats(
adapttempl.StreamingFormat(dashPropsCodec, dashboardPage), // chunked HTML
format.JSON(dashPropsCodec), // JSON fallback
)
The adapter detects IsStreamable() == true and calls MarshalTo(props, w) — the component writes directly to ResponseWriter without buffering. Headers are committed only after validation passes.
templ SSR format plug-in¶
adapters/templ bridges a templ component into the existing content negotiation pipeline. Add adapttempl.Format to a route's Formats and the same handler serves HTML to browser clients and JSON to API clients — no separate route, no separate handler.
import adapttempl "github.com/DaniDeer/go-codex/adapters/templ"
// Register both formats on one route — same handler, same route.
articleRoute = articleRoute.WithFormats(
adapttempl.Format(articlePropsCodec, ArticleCard), // Accept: text/html
format.JSON(articlePropsCodec), // Accept: application/json
)
// One handler, one route — the adapter picks the format from the Accept header.
nethttp.Register(mux, articleRoute, func(ctx context.Context, _ struct{}) (ArticleProps, error) {
return svc.GetArticle(ctx)
}, nethttp.Options{Observer: obs})
Key properties:
- Props are validated via the response codec's Refine constraints before the component renders. Invalid props return HTTP 500; the template is never reached with bad data.
- Works with both adapters/nethttp and adapters/chi — no adapter-specific variant needed.
- adapttempl.DecodeNotSupportedError is returned by the format's Unmarshal; use errors.As to detect it.
- Components written with templ.ComponentFunc require no code generation — self-contained in any .go file.
SSE with HTML fragments (HTMX-style)¶
Combine rest.NewSSERoute and adapttempl.Format to stream HTML fragments over SSE — the HTML-over-the-wire / HTMX sse-swap pattern:
// Each SSE event's data: field contains a rendered HTML fragment.
notifRoute, _ := rest.NewSSERoute[struct{}, NotifProps]("/sse/notifications",
codex.Empty, notifCodec, rest.RouteMeta{},
).Register(b)
notifRoute = notifRoute.WithFormats(
adapttempl.Format(notifCodec, notifFragment), // data: <li class="notif-warn">...</li>
)
Events with invalid props are rejected by the codec before the fragment component renders — no malformed HTML is ever sent to the client.
See also¶
- Guide: HTTP Server — SSE and templ SSR in the full HTTP server guide
- Feature: Formats & Serialization —
format.NewStreamedreference - examples/adapters-sse — SSE with BuildPath, invalid event rejection, observer
- examples/adapters-streaming-sse-templ — chunked streaming + SSE HTML fragments
- examples/adapters-templ — content negotiation: HTML + JSON from one route