Skip to content

Server

HandleSSE is a dedicated handler for Server-Sent Events that gives you a typed writer, automatic SSE headers, and end-to-end type safety from Go to TypeScript — including a generated sse helper and framework-specific hooks.

shiftapi.HandleSSE(api, "GET /events", func(r *http.Request, _ struct{}, sse *shiftapi.SSEWriter) error {
for msg := range messages(r.Context()) {
if err := sse.Send(msg); err != nil {
return err
}
}
return nil
}, shiftapi.SSESends(
shiftapi.SSEEventType[Message]("message"),
))

An SSE handler has one type parameter — input:

func handler(r *http.Request, in InputType, sse *shiftapi.SSEWriter) error
  • r *http.Request — the standard HTTP request
  • in InputType — automatically decoded from path, query, header, body, or form — identical to Handle
  • sse *shiftapi.SSEWriter — writer for sending events
  • error — return an error to stop the stream

Use struct{} when the handler takes no input:

shiftapi.HandleSSE(api, "GET /events", func(r *http.Request, _ struct{}, sse *shiftapi.SSEWriter) error {
// ...
}, shiftapi.SSESends(
shiftapi.SSEEventType[Event]("event"),
))

SSEWriter provides a single method for sending events:

Send automatically determines the event name from the concrete Go type registered via SSESends:

sse.Send(Message{Text: "hello"})

Produces:

event: message
data: {"text":"hello"}

Send JSON-encodes the value, writes it in SSE format, and flushes the response. On the first call, SSEWriter automatically sets the required headers:

  • Content-Type: text/event-stream
  • Cache-Control: no-cache
  • Connection: keep-alive

Input decoding works identically to Handle — all struct tags (path, query, header, json, form) and validation rules apply:

type EventInput struct {
Channel string `query:"channel" validate:"required"`
After int `query:"after"`
}
shiftapi.HandleSSE(api, "GET /events", func(r *http.Request, in EventInput, sse *shiftapi.SSEWriter) error {
for event := range subscribe(r.Context(), in.Channel, in.After) {
if err := sse.Send(event); err != nil {
return err
}
}
return nil
}, shiftapi.SSESends(
shiftapi.SSEEventType[Event]("event"),
))

Path parameters work too:

type StreamInput struct {
RoomID string `path:"room_id" validate:"required"`
}
shiftapi.HandleSSE(api, "GET /rooms/{room_id}/events", func(r *http.Request, in StreamInput, sse *shiftapi.SSEWriter) error {
// in.RoomID is parsed and validated
}, shiftapi.SSESends(
shiftapi.SSEEventType[Event]("event"),
))

Error behavior depends on whether the handler has started sending events:

  • Before sending — if your handler returns an error and hasn’t called Send, the framework handles it normally: matching WithError types, returning 422 for validation errors, or falling back to 500.
  • After sending — if events have already been sent, it’s too late to send an error response. The error is logged and the stream ends.
shiftapi.HandleSSE(api, "GET /events", func(r *http.Request, _ struct{}, sse *shiftapi.SSEWriter) error {
data, err := loadInitialData()
if err != nil {
return err // 500 (no events sent yet)
}
if err := sse.Send(Event{Data: data}); err != nil {
return err // logged, stream already started
}
return nil
},
shiftapi.SSESends(shiftapi.SSEEventType[Event]("event")),
shiftapi.WithError[*AuthError](http.StatusUnauthorized),
)

HandleSSE automatically generates the correct OpenAPI spec with text/event-stream as the response content type. The event types declared via SSESends are reflected into the schema:

type ChatEvent struct {
User string `json:"user"`
Message string `json:"message"`
}
shiftapi.HandleSSE(api, "GET /chat", chatHandler,
shiftapi.SSESends(shiftapi.SSEEventType[ChatEvent]("chat")),
)

Produces:

paths:
/chat:
get:
responses:
'200':
description: OK
content:
text/event-stream:
schema:
$ref: '#/components/schemas/ChatEvent'

You don’t need to use WithContentType or ResponseSchemaHandleSSE sets both automatically.

See SSE Client for the full TypeScript client documentation, including the generated sse() function, framework-specific examples, and discriminated unions.

For endpoints that emit multiple event types, use SSESends to declare each variant. This generates a oneOf schema with a discriminator in the OpenAPI spec, which produces TypeScript discriminated unions in the generated client.

Pass SSEEventType[T] descriptors to SSESends to declare each named event. When SSESends is used, Send automatically determines the event name from the concrete Go type — no need to pass the name manually:

type MessageData struct {
User string `json:"user"`
Text string `json:"text"`
}
type JoinData struct {
User string `json:"user"`
}
shiftapi.HandleSSE(api, "GET /chat", func(r *http.Request, _ struct{}, sse *shiftapi.SSEWriter) error {
if err := sse.Send(MessageData{User: "alice", Text: "hi"}); err != nil {
return err
}
return sse.Send(JoinData{User: "bob"})
}, shiftapi.SSESends(
shiftapi.SSEEventType[MessageData]("message"),
shiftapi.SSEEventType[JoinData]("join"),
))

SSESends produces a oneOf + discriminator schema:

paths:
/chat:
get:
responses:
'200':
content:
text/event-stream:
schema:
oneOf:
- type: object
required: [event, data]
properties:
event:
type: string
enum: [message]
data:
$ref: '#/components/schemas/MessageData'
- type: object
required: [event, data]
properties:
event:
type: string
enum: [join]
data:
$ref: '#/components/schemas/JoinData'
discriminator:
propertyName: event

All standard route options work with HandleSSE:

shiftapi.HandleSSE(api, "GET /events", handler,
shiftapi.SSESends(shiftapi.SSEEventType[Event]("event")),
shiftapi.WithRouteInfo(shiftapi.RouteInfo{
Summary: "Subscribe to events",
Tags: []string{"events"},
}),
shiftapi.WithError[*AuthError](http.StatusUnauthorized),
shiftapi.WithMiddleware(auth),
)
OptionDescription
WithRouteInfo(info)Set OpenAPI summary, description, and tags
WithError[T](code)Declare an error type at a status code
WithMiddleware(mw...)Apply HTTP middleware
WithResponseHeader(name, value)Set a static response header
SSESends(variants...)Declare discriminated union event types (see above)
Use caseRecommendation
SSE with typed eventsHandleSSE — typed writer, auto headers, typed TS client
SSE with custom framingHandleRaw + WithContentType("text/event-stream")
File downloadHandleRaw + WithContentType("application/octet-stream")
Bidirectional real-timeHandleWS — typed handlers, auto dispatch
JSON API endpointHandle

HandleSSE is the recommended approach for SSE. Use HandleRaw only when you need custom SSE framing or non-standard behavior.