Project: Multi-user Chat Server
This is the capstone project for Phase 3 and the final chapter of this book. Over the last twenty-one chapters, you have learned about variables, functions, types, enums, pattern matching, error handling, generics, closures, iterators, actors, concurrency, metaprogramming, and the standard library. Now it is time to put all of those skills together by building something real: a multi-user chat server.
A chat server is a program that accepts connections from multiple users, relays messages between them, and manages chat rooms. It is the kind of project that exists at the intersection of every major concept in this book. You need types to model messages, enums to represent the protocol, actors to handle concurrent connections, maps to track state, error handling to deal with disconnections, and the standard library to do the actual networking.
Here is what we will build, step by step:
- Message types -- define the protocol as an enum
- Server actor -- the central coordinator that manages clients
- Client handler -- per-connection logic for reading input
- Rooms -- multi-room chat channels with join and leave
- Main entry point -- wiring everything together
- Complete server -- the full program assembled
Setting Up
Create a new Auto project:
$ automan new chat_server
$ cd chat_serverThis is a larger project than the previous capstones, but the pattern is the same: build incrementally, test at each step, and add one piece at a time.
Design Overview
Before writing any code, let us think about the architecture. A chat server has three main components:
The server actor -- a single actor that maintains the global state: which users are connected, which rooms exist, and who is in each room. All messages flow through this actor.
Client handlers -- one actor per connected user. Each handler reads input from its user, parses commands, and forwards messages to the server actor.
The message protocol -- an enum that defines every type of message the system handles: connect, disconnect, chat, private message, join room, leave room, room message, and system notifications.
This design follows the actor model from Chapter 15. The server is a single point of coordination. Client handlers are independent actors that communicate with the server through messages. No shared memory, no locks, no data races -- just actors sending messages to each other.
Step 1: Message Types
Every network protocol starts with a definition of the messages that flow through it. In Auto, we model this as an enum. Each variant represents a different kind of event in the chat system.
This prints:
Alice connected
Alice: Hello everyone!
[PM] Alice -> Bob: Hi Bob!
[SYS] Alice has joined the lobby.
Alice disconnectedLet us walk through the design decisions:
Connect(String)-- carries the username. When a user connects, the server registers them.Disconnect(String)-- carries the username. The server removes them from all rooms and notifies others.Chat(String, String)-- carries the sender's name and the message text. This is a public message broadcast to all users.PrivateMsg(String, String, String)-- carries the sender, the recipient, and the text. Only the recipient should see this.System(String)-- a notification from the server itself: "Alice joined", "Bob left", etc.
Why an enum? Because the compiler forces us to handle every variant. If we add a new message type later (say, Kick(String, String)), the compiler will warn us at every is expression that does not handle it. This is the exhaustive pattern matching we learned about in Chapter 7.
In C, this maps to a tagged union with a manual switch statement. In Python, it maps to a union of dataclasses with isinstance checks. In TypeScript, it maps to a discriminated union with switch on the kind field. In all cases, the pattern is the same: one type, many variants, exhaustive handling.
Step 2: Server Actor
The server actor is the heart of the chat system. It runs as a single actor (Remember actors from Chapter 15?) that maintains two pieces of state: a map of connected clients and a message loop.
This prints:
[SYS] Alice joined. Online: 1
[SYS] Bob joined. Online: 2
Alice: Hello Bob!
[PM] Bob -> Alice: Hi Alice!
[SYS] Bob left. Online: 1The server actor maintains a clients map that tracks who is online. Each variant in the is expression handles a different message type:
Connectadds the user to theclientsmap and announces their arrival.Disconnectremoves them and announces their departure.Chatprints the message (in a real server, it would broadcast to all clients).PrivateMsgprints a private message indicator.Systemprints a server notification.
The key insight is that all state changes happen inside a single actor. The clients map is private to the server function -- no other actor can touch it. This eliminates data races without any locks or mutexes. The actor model guarantees that only one message is processed at a time.
In Rust, the server runs in a spawned thread with an mpsc::Receiver. In Python, it runs in a daemon thread with a queue.Queue. In C, we simulate the message loop with an array of messages. In TypeScript, we process messages synchronously in an array. The Auto version is the cleanest: spawn server, send server Message.Connect(...). Two lines.
Step 3: Client Handler
Each connected user needs their own actor that reads input and forwards messages to the server. This is the client handler.
The client handler is where user input gets parsed and turned into protocol messages. Here is what it handles:
/quit-- sends aDisconnectmessage and returns from the function, which terminates the actor. This is how a user leaves cleanly./msg <name> <text>-- parses the command into a target username and message text, then sends aPrivateMsgto the server. Thesplit(" ")call (from the standard library we toured in Chapter 21) breaks the input into parts, andparts[2..].join(" ")reassembles everything after the name.Plain text -- anything that is not a command gets sent as a
Chatmessage to the server.
Notice the return statement after /quit. In an actor function, returning from the function terminates the actor. This is the clean way to shut down an actor: process the final message, then return.
Step 4: Rooms
A real chat server needs rooms (also called channels). Users can join a room to talk to everyone in it, and leave when they are done. Let us add room management to our message protocol and server.
This prints:
[SYS] Alice joined. Online: 1
[SYS] Bob joined. Online: 2
[SYS] Alice joined #random
[SYS] Bob joined #random
[#random] Alice: Anyone here?
(delivered to 2 members)
[#random] Bob: Yes!
(delivered to 2 members)
[SYS] Alice left #randomWe added three new message types:
JoinRoom(String, String)-- the first field is the username, the second is the room name. The server checks that the room exists and that the user is not already in it before adding them.LeaveRoom(String, String)-- removes the user from the room's member list.RoomMsg(String, String, String)-- a message targeted at a specific room. The first field is the room name, the second is the sender, and the third is the text. The server looks up the room's member list and would deliver the message to each member.
The rooms map uses Map<String, List<String>> -- a map from room names to lists of usernames. This is a nested collection, the kind we worked with in Chapter 4 and Chapter 21. Each room name maps to a list of the users currently in that room.
Notice how the Disconnect handler now iterates over all rooms to remove the disconnected user from every room they were in. This is the for room in rooms syntax from the iterators chapter. It visits every key-value pair in the map.
In C, this nested data structure requires arrays of arrays with manual size tracking. In Rust, it is HashMap<String, Vec<String>>. In Python, a dict of lists. In TypeScript, a Map<string, string[]>. Auto's version is the most readable: Map<String, List<String>>.
Step 5: Wiring It Together
Now let us assemble the pieces into a working program. The main function starts the server actor and provides a console for server administration.
The main function now serves as the server's control console. It starts the server actor, prints a banner, and enters a command loop:
/quit-- shuts down the server by returning frommain, which terminates the program./status-- prints the server's port number.- Unknown commands -- prints an error message.
The main function is itself an actor (as we learned in Chapter 15), so it uses on input String to receive console input. This is the same pattern we used in every actor: a loop with on to receive messages.
The use std::net import at the top prepares us for real networking. In a production server, the main function would open a TCP listener on the port and spawn a client_handler actor for each incoming connection.
Step 6: Complete Server
Here is the complete program assembled. This is the payoff -- everything we have built comes together in a single, clean Auto program.
The complete server has several improvements over the earlier versions:
Name collision detection -- the
Connecthandler checksclients.contains(name)and rejects duplicate usernames. This is a real validation concern that every chat server must handle.Broadcast and broadcast_room -- helper functions that deliver messages to all connected clients or all members of a specific room. In a real server, these would iterate over the client list and send a message to each one.
Welcome messages -- when a user connects, the server sends them a personal "Welcome" message via
send caller, and broadcasts their arrival to everyone else.Error handling for private messages -- the
PrivateMsghandler checks whether the recipient exists before delivering. If not, it sends a "User not found" system message.Client commands -- the
client_handlernow supports/join,/leave,/rooms, and/msgcommands, making it a fully interactive client.
The C version uses pthread_create and pthread_join to run client handlers as real OS threads. The Rust version uses std::thread::spawn with cloned channel senders. The Python version uses threading.Thread with daemon threads. The TypeScript version runs client handlers sequentially, since TypeScript's single-threaded event loop handles concurrency differently.
Testing the Server
There are two ways to test a chat server: manual testing and automated tests.
Manual Testing
Run the server and connect with multiple terminals:
$ automan run
=== AutoChat Server v1.0 ===
Starting on port 8080...
Server running. Press Ctrl+C to stop.In a real deployment, you would connect with separate client processes. For development, the simulated client handlers in our code are sufficient.
Automated Tests
Auto supports #[test] attributes for unit testing. Here are the tests we would write:
#[test]
fn test_connect_increments_client_count() {
var clients: Map<String, int> = {}
clients["Alice"] = 1
assert clients.len() == 1
clients["Bob"] = 1
assert clients.len() == 2
}
#[test]
fn test_disconnect_removes_client() {
var clients: Map<String, int> = {}
clients["Alice"] = 1
clients.remove("Alice")
assert clients.len() == 0
}
#[test]
fn test_join_room_adds_member() {
var rooms: Map<String, List<String>> = { "general": [] }
rooms["general"].push("Alice".to_string())
assert rooms["general"].len() == 1
}
#[test]
fn test_leave_room_removes_member() {
var rooms: Map<String, List<String>> = { "general": [] }
rooms["general"].push("Alice".to_string())
rooms["general"].remove("Alice")
assert rooms["general"].len() == 0
}These tests verify the core data operations without needing a running server. They test the state transitions that the server actor performs.
Taking It Further
This chat server is a solid foundation. Here are some ideas for extending it:
Authentication -- add a
Login(String, String)message that checks credentials before allowing a connection. Store passwords as hashes usingstd::crypto.Persistence -- save chat history to a file using
write_filefromstd::fs. Load it back on server restart withread_file.WebSocket support -- use
std::netto accept WebSocket connections instead of raw TCP. This lets browsers connect directly.Admin commands -- add
/kick,/ban, and/create-roomcommands that only administrators can use.Message history -- store the last N messages per room and send them to new users when they join.
Typing indicators -- add a
Typing(String, String)message that shows "Alice is typing..." to other room members.Rate limiting -- prevent spam by limiting how many messages a user can send per second.
Encryption -- encrypt private messages end-to-end using
std::cryptoso that even the server cannot read them.
Each of these extensions exercises different parts of Auto's standard library and type system. They are excellent practice projects.
The Journey from Hello, World!
Let us take a moment to reflect on how far we have come.
In Chapter 1, you wrote your first Auto program: print("Hello, World!"). A single line of code that printed a string and exited. You did not know about variables, functions, or types yet.
Now, in this final chapter, you have built a multi-user chat server with:
- Enums (Chapter 7) to define the message protocol
- Pattern matching (Chapter 7) to handle each message type
- Maps and Lists (Chapter 4) to track clients and rooms
- String methods (Chapter 21) to parse commands
- Actors (Chapter 15) for concurrent server and client handling
- The standard library (Chapter 21) for
format!,split,join,contains, andstarts_with - Error handling (Chapter 12) for missing users and invalid rooms
- Functions (Chapter 2) to organize the code into
server,client_handler,broadcast, andbroadcast_room
You did not just learn Auto. You learned the fundamental concepts that underpin every programming language: types, functions, collections, control flow, concurrency, and error handling. Auto's syntax maps cleanly to Rust, Python, C, and TypeScript, so everything you learned transfers directly.
The skills you have built in this book -- designing data structures, writing clean functions, handling errors explicitly, reasoning about concurrency -- are the skills that professional software engineers use every day. Whether you build web servers, embedded systems, data pipelines, or operating systems, the patterns you have learned here will serve you well.
Thank you for reading. Now go build something amazing.
Practical Network Patterns
Beyond the chat server, here are three common network utility patterns: parsing URLs, extracting query parameters, and collecting process output.
Summary
This capstone project brought together every concept from Phase 3 and the entire book:
| Concept | Where We Used It |
|---|---|
| Enums | Message enum defines the protocol |
| Pattern matching | is msg { Connect(name) => ... } |
| Maps | clients: Map<String, int>, rooms: Map<String, List<String>> |
| Lists | Room member lists, message parts |
| String methods | split, join, starts_with, contains, to_string |
| Actors | spawn server, spawn client_handler |
| Message passing | send server Message.Connect(...) |
on receive | on msg Message { ... }, on input String { ... } |
| Error handling | Name collision check, missing user check |
| Standard library | std::net, format!, println |
| Functions | server, client_handler, broadcast, broadcast_room |
| Types | Map<String, List<String>>, Message variants |
Key takeaways:
Design the protocol first -- the
Messageenum is the contract between all components. Get it right, and everything else follows.One actor owns the state -- the server actor is the single source of truth for clients and rooms. This eliminates an entire class of concurrency bugs.
Build incrementally -- we started with message types, then the server, then client handlers, then rooms, then the complete program. Each step was testable on its own.
The actor model scales naturally -- adding more features (auth, persistence, admin commands) means adding more message types and more handlers. The architecture stays the same.
Error handling is not an afterthought -- we check for duplicate names, missing users, and nonexistent rooms. These checks are part of the normal control flow, not bolted on afterward.