This episode takes you beyond REST: realtime communication with WebSocket via gorilla/websocket, Server-Sent Events for one-way streaming, and mounting non-REST handlers such as gRPC-gateway and GraphQL. You will also build a mixed service on a single http.Server.

REST handles request-response, but many needs live outside it: push notifications, realtime chat, streaming progress bars, and RPC services. Episode 16 shows how chi stays relevant in that world — because any router that implements http.Handler can be mounted at any path.
WebSocket, SSE, gRPC-gateway, and GraphQL all boil down to HTTP. With chi, you can combine them all in one server without conflicts.
Install and build a WebSocket endpoint:
go get github.com/gorilla/websocketvar upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(req *http.Request) bool {
return true
},
}
r.Get("/ws", func(w http.ResponseWriter, req *http.Request) {
conn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
return
}
defer conn.Close()
for {
_, msg, err := conn.ReadMessage()
if err != nil {
return
}
conn.WriteMessage(websocket.TextMessage, msg)
}
})upgrader.Upgrade(w, req, nil) turns the HTTP connection into a WebSocket. The loop reads and writes messages — a simple echo server ready to grow into a chat.
A CheckOrigin that returns true is convenient for development, but in production it must be verified:
CheckOrigin: func(req *http.Request) bool {
origin := req.Header.Get("Origin")
return origin == "https://app.example.com"
}req.Header.Get("Origin") is checked against an allowlist — preventing other sites from opening WebSocket connections to your server.
SSE streams data from the server to the client over plain HTTP:
r.Get("/events", func(w http.ResponseWriter, req *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming tidak didukung",
http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Fprintf(w, "data: ping\n\n")
flusher.Flush()
case <-req.Context().Done():
return
}
}
})fmt.Fprintf(w, "data: ping\n\n") writes an event in SSE format. <-req.Context().Done() stops the stream when the client leaves — exactly the pattern from episode 12.
If you only need push notifications, SSE is simpler than WebSocket.
gRPC-gateway bridges protobuf to REST/HTTP. Its build output is an http.Handler — just mount it:
gw, err := gateway.NewGateway(ctx)
r.Mount("/v1", gw)r.Mount("/v1", gw) attaches a gateway that returns an http.Handler. REST clients call gRPC endpoints via /v1/... paths without knowing the protocol behind them.
A GraphQL server is also just an http.Handler:
srv := handler.NewDefaultServer(graphqlSchema)
r.Handle("/graphql", srv)handler.NewDefaultServer(graphqlSchema) creates a GraphQL server (e.g., gqlgen), and r.Handle("/graphql", srv) registers it. REST and GraphQL routes coexist on the same router.
Combine everything in a single http.Server:
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/ws", wsHandler)
r.Get("/events", sseHandler)
r.Handle("/graphql", graphQLHandler)
r.Mount("/v1", gatewayHandler)
r.Get("/health", healthHandler)
http.ListenAndServe(":8080", r)
}r.Handle("/graphql", graphQLHandler) and r.Mount("/v1", gatewayHandler) attach all protocols to one router. One port, one middleware stack, one lifecycle — operations become much simpler.
All of these protocols end up as an http.Handler. That's the strength of chi's design: it doesn't need to know what happens inside a handler, it just forwards it. As long as something can become an http.Handler, chi can route it.
Key takeaways:
upgrader.Upgrade then a read-write loop.CheckOrigin to protect WebSocket connections.http.Flusher and the data: ... format.req.Context().Done() stops streaming when the client leaves.http.Handlers that you just mount.In the next episode 17 we make sure none of it breaks: testing and benchmark — httptest for handlers and middleware, testing subrouters, mocking services, and benchmarking with testing.B to compare route performance.