Vibe Engines
YouTube

Lesson 7.3 · 8 min

Splitting code into modules

Chatterbox is one file and it is getting long. Rust lets you put related code in named boxes called modules, and decide what each box shows to the rest of the program.

Adds to Chatterboxmessage.rs and command.rs split out of main.rs.

Boxes with a front door

Chatterbox now has a message type, a command parser and an error type. Everything sits in one list in main.rs. A module is a named box around some of that code. Writing mod message { ... } creates a box called message.

The rule that makes boxes useful: everything inside is private by default. Code outside the box cannot see it. You choose what to show by writing pub (short for public) in front of it. That way message can keep its details to itself and only offer a small, stable surface.

Reaching into a private field
mod message {
pub struct Message {
text: String,
}
 
impl Message {
pub fn new(text: &str) -> Message {
Message { text: text.to_string() }
}
}
}
 
fn main() {
let m = message::Message::new("hi");
println!("{}", m.text);
}
error[E0616]: field `text` of struct `Message` is private
  --> src/main.rs:15:22
   |
15 |     println!("{}", m.text);
   |                      ^^^^ private field

The struct is pub, so main may name it and call new. The field text is not, so it is off limits. Two fixes: write pub text: String to show it, or add a pub fn text(&self) -> &str that shows it on your terms. The second keeps the freedom to change how the text is stored later.

Walking in with use

Outside a module you reach its items with a path: message::Message. Typing that every time gets tiring, so use message::Message; brings the name into scope once. It is the same job as Python's from message import Message.

This listing is trimmed to the part this lesson is about; the commands from earlier chapters (/say, /history, /stats, /long) and the per-author counts are still in your project.

Chatterbox v0.7: two modules in one file (trimmed)
mod message {
pub struct Message {
pub author: String,
pub text: String,
}
 
impl Message {
pub fn new(author: &str, text: &str) -> Message {
Message { author: author.to_string(), text: text.to_string() }
}
}
}
 
mod command {
#[derive(Debug, PartialEq)]
pub enum Command {
Help,
Quit,
Last(u32),
}
 
pub fn parse(line: &str) -> Option<Command> {
match line.trim() {
"/help" => Some(Command::Help),
"/quit" => Some(Command::Quit),
_ => None,
}
}
}
 
use command::Command;
use message::Message;
 
fn main() {
let m = Message::new("sam", "/help");
println!("{}: {}", m.author, m.text);
println!("{:?}", command::parse(&m.text));
println!("{:?}", Command::Last(3));
}
$ cargo run
sam: /help
Some(Help)
Last(3)

From mod to file

Everything above lived in one file so you can run it in the browser. In a real Cargo project the box usually becomes a file. Take the body of mod message { ... }, move it into src/message.rs, and replace it in main.rs with the single line mod message;. The semicolon form tells Rust "this module's contents are in the file with the same name". Nothing else changes: pub and use work exactly as before.

chatterbox/
  Cargo.toml
  src/
    main.rs       // mod message; mod command; fn main() {...}
    message.rs    // Message
    command.rs    // Command, parse

An outside module can only reach into another one through its pub items, which is why file boundaries stay clean as a project grows.

Try itYour turn: fill the command module

Finish command::parse. /help and /quit give their variants, /last N gives Last(N) when N is a number, and everything else (including /last x) gives None. Use strip_prefix, which returns the rest of the text if the prefix is there.

Finish command::parse. /help and /quit give their variants, /last N gives Last(N) when N is a number, and everything else (including /last x) gives None. Use strip_prefix, which returns the rest of the text if the prefix is there.

mod command {
#[derive(Debug, PartialEq)]
pub enum Command {
Help,
Quit,
Last(u32),
}
 
pub fn parse(line: &str) -> Option<Command> {
// /help, /quit, "/last N", else None
None
}
}
 
fn main() {
println!("{:?}", command::parse("/last 3"));
println!("{:?}", command::parse("hello"));
}

Hidden tests (4): last_with_a_number help_and_quit last_with_junk_is_none plain_text_is_none

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

What you have now

Chatterbox has two named boxes with a small public surface each. As a preview, the next lesson writes the history to disk so it is still there tomorrow.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What is the default visibility of an item inside a mod?Reveal
Private: only code inside the module can use it. Add pub to expose it.
What does `mod message;` (with a semicolon) mean in main.rs?Reveal
The contents of module message live in the file src/message.rs.
What does use do?Reveal
It brings a name into scope so you do not have to write its full path each time.

That is lesson 7.3. Tick it off so the course remembers where you are.

Learn Rust by building a chat app · Lesson 7.3 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026