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.
Registering SSE handlers
Section titled “Registering SSE handlers”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"),))Handler signature
Section titled “Handler signature”An SSE handler has one type parameter — input:
func handler(r *http.Request, in InputType, sse *shiftapi.SSEWriter) errorr *http.Request— the standard HTTP requestin InputType— automatically decoded from path, query, header, body, or form — identical toHandlesse *shiftapi.SSEWriter— writer for sending eventserror— 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
Section titled “SSEWriter”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: messagedata: {"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-streamCache-Control: no-cacheConnection: keep-alive
Input parsing
Section titled “Input parsing”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 handling
Section titled “Error handling”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: matchingWithErrortypes, returning422for validation errors, or falling back to500. - 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),)OpenAPI spec
Section titled “OpenAPI spec”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 ResponseSchema — HandleSSE sets both automatically.
TypeScript client
Section titled “TypeScript client”See SSE Client for the full TypeScript client documentation, including the generated sse() function, framework-specific examples, and discriminated unions.
Discriminated union events
Section titled “Discriminated union events”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.
Register with SSESends
Section titled “Register with SSESends”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"),))Generated OpenAPI schema
Section titled “Generated OpenAPI schema”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: eventRoute options
Section titled “Route options”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),)| Option | Description |
|---|---|
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) |
When to use HandleSSE vs HandleRaw
Section titled “When to use HandleSSE vs HandleRaw”| Use case | Recommendation |
|---|---|
| SSE with typed events | HandleSSE — typed writer, auto headers, typed TS client |
| SSE with custom framing | HandleRaw + WithContentType("text/event-stream") |
| File download | HandleRaw + WithContentType("application/octet-stream") |
| Bidirectional real-time | HandleWS — typed handlers, auto dispatch |
| JSON API endpoint | Handle |
HandleSSE is the recommended approach for SSE. Use HandleRaw only when you need custom SSE framing or non-standard behavior.