Skip to content
📖 The Auto Programming LanguageBook contents

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:

  1. Message types -- define the protocol as an enum
  2. Server actor -- the central coordinator that manages clients
  3. Client handler -- per-connection logic for reading input
  4. Rooms -- multi-room chat channels with join and leave
  5. Main entry point -- wiring everything together
  6. Complete server -- the full program assembled

Setting Up ​

Create a new Auto project:

console
$ automan new chat_server
$ cd chat_server

This 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:

  1. 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.

  2. Client handlers -- one actor per connected user. Each handler reads input from its user, parses commands, and forwards messages to the server actor.

  3. 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.

Chat message types as an enum with pattern matching

This prints:

Alice connected
Alice: Hello everyone!
[PM] Alice -> Bob: Hi Bob!
[SYS] Alice has joined the lobby.
Alice disconnected

Let us walk through the design decisions:

  1. Connect(String) -- carries the username. When a user connects, the server registers them.

  2. Disconnect(String) -- carries the username. The server removes them from all rooms and notifies others.

  3. Chat(String, String) -- carries the sender's name and the message text. This is a public message broadcast to all users.

  4. PrivateMsg(String, String, String) -- carries the sender, the recipient, and the text. Only the recipient should see this.

  5. 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.

The server actor managing connected clients

This prints:

[SYS] Alice joined. Online: 1
[SYS] Bob joined. Online: 2
Alice: Hello Bob!
[PM] Bob -> Alice: Hi Alice!
[SYS] Bob left. Online: 1

The server actor maintains a clients map that tracks who is online. Each variant in the is expression handles a different message type:

  • Connect adds the user to the clients map and announces their arrival.
  • Disconnect removes them and announces their departure.
  • Chat prints the message (in a real server, it would broadcast to all clients).
  • PrivateMsg prints a private message indicator.
  • System prints 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.

Per-client actor that reads input and parses commands

The client handler is where user input gets parsed and turned into protocol messages. Here is what it handles:

  1. /quit -- sends a Disconnect message and returns from the function, which terminates the actor. This is how a user leaves cleanly.

  2. /msg <name> <text> -- parses the command into a target username and message text, then sends a PrivateMsg to the server. The split(" ") call (from the standard library we toured in Chapter 21) breaks the input into parts, and parts[2..].join(" ") reassembles everything after the name.

  3. Plain text -- anything that is not a command gets sent as a Chat message 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.

Room management: join, leave, and broadcast to rooms

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 #random

We 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.

Main entry point: starting the server and accepting admin commands

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 from main, 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 multi-user chat server

The complete server has several improvements over the earlier versions:

  1. Name collision detection -- the Connect handler checks clients.contains(name) and rejects duplicate usernames. This is a real validation concern that every chat server must handle.

  2. 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.

  3. Welcome messages -- when a user connects, the server sends them a personal "Welcome" message via send caller, and broadcasts their arrival to everyone else.

  4. Error handling for private messages -- the PrivateMsg handler checks whether the recipient exists before delivering. If not, it sends a "User not found" system message.

  5. Client commands -- the client_handler now supports /join, /leave, /rooms, and /msg commands, 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:

console
$ 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:

auto
#[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:

  1. Authentication -- add a Login(String, String) message that checks credentials before allowing a connection. Store passwords as hashes using std::crypto.

  2. Persistence -- save chat history to a file using write_file from std::fs. Load it back on server restart with read_file.

  3. WebSocket support -- use std::net to accept WebSocket connections instead of raw TCP. This lets browsers connect directly.

  4. Admin commands -- add /kick, /ban, and /create-room commands that only administrators can use.

  5. Message history -- store the last N messages per room and send them to new users when they join.

  6. Typing indicators -- add a Typing(String, String) message that shows "Alice is typing..." to other room members.

  7. Rate limiting -- prevent spam by limiting how many messages a user can send per second.

  8. Encryption -- encrypt private messages end-to-end using std::crypto so 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, and starts_with
  • Error handling (Chapter 12) for missing users and invalid rooms
  • Functions (Chapter 2) to organize the code into server, client_handler, broadcast, and broadcast_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.

Parsing a URL into components
Extracting query parameters from a URL
Collecting process output with a List

Summary ​

This capstone project brought together every concept from Phase 3 and the entire book:

ConceptWhere We Used It
EnumsMessage enum defines the protocol
Pattern matchingis msg { Connect(name) =&gt; ... }
Mapsclients: Map&lt;String, int&gt;, rooms: Map&lt;String, List&lt;String&gt;&gt;
ListsRoom member lists, message parts
String methodssplit, join, starts_with, contains, to_string
Actorsspawn server, spawn client_handler
Message passingsend server Message.Connect(...)
on receiveon msg Message { ... }, on input String { ... }
Error handlingName collision check, missing user check
Standard librarystd::net, format!, println
Functionsserver, client_handler, broadcast, broadcast_room
TypesMap&lt;String, List&lt;String&gt;&gt;, Message variants

Key takeaways:

  1. Design the protocol first -- the Message enum is the contract between all components. Get it right, and everything else follows.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Released under the MIT License.