Quick start
A chat server with rooms, in one file. You need Go 1.25 or later, and any WebSocket client to try it: npx wscat is used below.
1. Set up
mkdir chat && cd chat
go mod init example.com/chat
go get go.chanx.dev2. Declare the messages
Create main.go. A message is a struct embedding chanx.BaseMessage, with its action in the tag and its body in a Payload field:
type ChatPayload struct {
Text string `json:"text"`
}
// ChatMessage is what a client sends.
type ChatMessage struct {
chanx.BaseMessage `action:"chat" description:"Send a message to the room"`
Payload ChatPayload `json:"payload"`
}
// ChatNotification is what everyone in the room receives.
type ChatNotification struct {
chanx.BaseMessage `action:"chat_notification"`
Payload ChatPayload `json:"payload"`
}3. Write the consumer
A consumer serves one route. Each exported method taking a *chanx.Context and a message is a handler, picked by the message's action:
type ChatConsumer struct{}
// Groups puts each connection in its room, from the {room} path parameter.
func (ChatConsumer) Groups(c *chanx.Conn) []string { return []string{"room." + c.Param("room")} }
// Emits declares what the consumer sends besides replies.
func (ChatConsumer) Emits() []chanx.Message { return []chanx.Message{ChatNotification{}} }
func (ChatConsumer) HandleChat(ctx *chanx.Context, m ChatMessage) error {
return ctx.Broadcast(ChatNotification{Payload: m.Payload})
}
func (ChatConsumer) HandlePing(ctx *chanx.Context, _ chanx.PingMessage) (chanx.PongMessage, error) {
return chanx.PongMessage{}, nil
}What each part does:
Groupsputs each connection in its room.{room}is a path parameter, read withc.Param("room").HandleChatruns for everychatframe, after its payload was validated, and broadcasts to the room: the sender included, everyone connected to the same room.HandlePingreturns a reply. A reply goes back to the sender and carries therefof the frame it answers.Emitsdeclares what the consumer broadcasts, so the AsyncAPI document lists it.
4. Mount and run
func newEngine() *chanx.Engine {
engine := chanx.New(chanx.Config{}) // an in-memory layer; Redis for several processes
engine.Handle("/ws/chat/{room}", ChatConsumer{}, chanx.Name("chat"))
return engine
}
func main() {
engine := newEngine()
mux := http.NewServeMux()
chanxhttp.Mount(mux, engine)
chanxhttp.MountDocs(mux, engine, "/asyncapi", asyncapi.Info{Title: "Chat"})
log.Println("listening on http://localhost:8000")
log.Fatal(http.ListenAndServe(":8000", mux))
}go run .chanx.New gives the engine an in-memory channel layer, enough for one process. chanxhttp.Mount registers the route on the ServeMux; gin, echo and Fiber have an adapter each, see Frameworks.
5. Talk to it
Open two terminals:
npx wscat -c ws://localhost:8000/ws/chat/lobbySend from one:
{"action": "chat", "payload": {"text": "hi"}}Both receive:
{"action": "chat_notification", "payload": {"text": "hi"}}Ask with a ref, and the reply carries it back:
> {"action": "ping", "ref": "1"}
< {"action": "pong", "payload": null, "version": 1, "ref": "1"}6. Try breaking it
Misspell the field:
> {"action": "chat", "payload": {"txt": "hi"}}
< {"action": "error", "payload": [{"type": "missing", "loc": ["chat", "payload", "text"], "msg": "Field required"}]}HandleChat never ran. The error lists every problem with its location, in the same format as Python chanx, so a client handles errors once for every server.
Now open http://localhost:8000/asyncapi: the API you wrote, documented from the code, with chat, chat_notification and ping and their schemas.
Next
- Tutorial: a chat server: authentication, presence, tests and Redis, step by step.
- Messages and validation: optional fields, defaults, enums, custom checks.
- Clients: a typed TypeScript client generated from
/asyncapi.json.