Ever wondered what it's like to build a real-world Rust project as a beginner? This tutorial documents the actual journey of building nginx-discovery - a professional NGINX configuration parser and CLI tool - using AI as a learning companion.
Unlike typical tutorials that show you the "perfect" final code, this guide shows you:
- ✅ The actual errors we encountered
- ✅ Why each crate was chosen
- ✅ How design decisions were made
- ✅ What those cryptic compiler errors mean
- ✅ How to learn Rust effectively with AI assistance
Perfect for beginners who want to understand not just what to code, but why and how to think through problems.
Table of Contents
- The Project Overview
- Understanding the Crate Ecosystem
- Common Rust Errors and How to Fix Them
- Design Decisions Explained
- The Build Process
- Learning Resources
- Tips for Learning Rust with AI
The Project Overview
What We're Building
nginx-discovery is a CLI tool and library that:
- Parses NGINX configuration files
- Extracts servers, locations, logs
- Analyzes SSL/TLS configuration
- Performs security audits
- Provides an interactive exploration mode
Tech Stack
Language: Rust (edition 2021)
Binary Size: ~8MB (release build)
Dependencies: 15+ crates
Lines of Code: ~5000+
Time to Build: 4-6 weeks (part-time)
Why This Project is Great for Learning
- Real-world problem - Actual DevOps use case
- Multiple concepts - Parsing, CLI, data structures
- Error handling - Learn Result properly
- Type system - See Rust's types in action
- Ecosystem - Use popular crates
- Testing - Write real tests
Understanding the Crate Ecosystem
Let's break down every crate we used and WHY we chose it.
Core Dependencies
1. thiserror (Error Handling)
What it does: Simplifies creating custom error types
Why we chose it:
- Reduces boilerplate for error types
- Works well with
Result<T, E> - Industry standard (used by thousands of crates)
Example:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ParseError {
#[error("unexpected EOF at line {line}")]
UnexpectedEof { line: usize },
#[error("syntax error: {0}")]
Syntax(String),
}
Learning Resources:
When to use: Any time you need custom error types. Much better than writing manual Display/Error implementations.
2. serde + serde_json + serde_yaml (Serialization)
What it does: Converts Rust structs to/from JSON, YAML, etc.
Why we chose it:
- De facto standard for serialization
- Zero-cost abstractions
- Works with derive macros (easy!)
Example:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Server {
name: String,
port: u16,
}
// Now you can do:
let json = serde_json::to_string(&server)?;
let yaml = serde_yaml::to_string(&server)?;
Learning Resources:
Gotcha: Remember the #[cfg_attr(feature = "serde", derive(...))] pattern for optional serialization!
CLI Dependencies
3. clap (Command-Line Argument Parsing)
What it does: Parses command-line arguments and generates help text
Why we chose it:
- Most popular CLI framework in Rust
- Derive macros make it easy
- Automatic help generation
- Subcommands, flags, options all built-in
Example:
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "nginx-discover")]
#[command(version, about)]
struct Cli {
/// Path to nginx.conf
#[arg(short, long)]
config: Option<PathBuf>,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Parse configuration
Parse,
/// Extract information
Extract,
}
Learning Resources:
Common Error: Forgetting #[command(subcommand)] for nested commands!
4. colored (Terminal Colors)
What it does: Adds colors to terminal output
Why we chose it:
- Simple API
- Cross-platform
- Widely used
Example:
use colored::Colorize;
println!("{}", "Success!".green());
println!("{}", "Warning!".yellow());
println!("{}", "Error!".red().bold());
Learning Resources:
Tip: Use colored::control::set_override(false) to disable colors when piping output!
5. tabled (Table Formatting)
What it does: Creates beautiful ASCII tables
Why we chose it:
- Easy to use
- Lots of styling options
- Good performance
Example:
use tabled::{Table, Tabled};
#[derive(Tabled)]
struct Server {
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Port")]
port: u16,
}
let servers = vec![...];
let table = Table::new(servers);
println!("{}", table);
Learning Resources:
6. dialoguer (Interactive Prompts)
What it does: Creates interactive CLI prompts (menus, inputs, confirmations)
Why we chose it:
- Beautiful UI
- Easy to use
- Good theming support
Example:
use dialoguer::{Select, Input, Confirm};
let selection = Select::new()
.with_prompt("What would you like to do?")
.items(&["Parse", "Extract", "Exit"])
.interact()?;
let name: String = Input::new()
.with_prompt("Server name")
.interact()?;
let confirmed = Confirm::new()
.with_prompt("Continue?")
.interact()?;
Learning Resources:
Utility Dependencies
7. anyhow (Error Handling for Applications)
What it does: Simplified error handling for applications (not libraries)
Why we chose it:
- Use for the CLI binary (not the library)
- Great for quick prototyping
- Provides context to errors
Example:
use anyhow::{Context, Result};
fn load_config(path: &Path) -> Result<Config> {
std::fs::read_to_string(path)
.context("Failed to read config file")?
// ...
}
Learning Resources:
Important: Use thiserror for libraries, anyhow for applications!
8. which (Finding Executables)
What it does: Finds executables in PATH (like which command)
Why we chose it:
- Needed to find
nginxbinary - Cross-platform
- Simple API
Example:
use which::which;
match which("nginx") {
Ok(path) => println!("Found nginx at: {}", path.display()),
Err(_) => println!("nginx not found in PATH"),
}
Common Rust Errors and How to Fix Them
Let's look at the ACTUAL errors we encountered and how we fixed them.
Error 1: Borrow Checker - "Value borrowed after move"
The Error:
error[E0382]: borrow of moved value: `critical`
--> src/commands/analyze.rs:180:21
|
174 | let critical: Vec<_> = issues.iter()...
| -------- move occurs because `critical` has type `Vec<&Issue>`
180 | for issue in critical {
| -------- `critical` moved due to implicit call to `.into_iter()`
208 | critical.len(),
| ^^^^^^^^ value borrowed here after move
What it means:
- We created a Vec
- Used it in a for loop (which consumed it)
- Tried to use it again (can't! it's gone)
The Fix:
// BAD:
for issue in critical { // Moves critical
// ...
}
println!("{}", critical.len()); // Error! critical was moved
// GOOD:
for issue in &critical { // Borrows critical
// ...
}
println!("{}", critical.len()); // Works! critical still exists
Key Lesson: Add & to borrow instead of move!
Learning Resources:
Error 2: Clippy - "collapsible_if"
The Warning:
warning: this `if` statement can be collapsed
|
268 | if condition1 {
269 | if condition2 {
| ^^^^^^^^^^^^^^
What it means:
Nested ifs can be combined with &&
The Fix:
// BAD:
if server.names.is_empty() {
if server.is_default() {
// ...
}
}
// GOOD:
if server.names.is_empty() && server.is_default() {
// ...
}
Key Lesson: Clippy helps you write idiomatic Rust!
Error 3: Type Mismatch - "expected X, found Y"
The Error:
error[E0308]: mismatched types
|
20 | Commands::Analyze(args) => analyze::run(args, &cli.global)?,
| ^^^^ expected `DoctorArgs`, found `AnalyzeArgs`
What it means:
Function expects one type, you gave it another
The Fix:
// Check the function signature:
pub fn run(args: DoctorArgs, ...) -> Result<()> {
^^^^^^^^^^^
// We were passing AnalyzeArgs!
// Fix: Change to correct type
pub fn run(args: AnalyzeArgs, ...) -> Result<()> {
^^^^^^^^^^^^
Key Lesson: Rust's type system catches these at compile time!
Learning Resources:
Error 4: Unused Result - "unused Result that must be used"
The Warning:
warning: unused `std::result::Result` that must be used
|
126 | print_summary(passed, warnings, errors);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
What it means:
Function returns Result<()>, but you're ignoring it
The Fix:
// BAD:
print_summary(passed, warnings, errors);
// GOOD:
print_summary(passed, warnings, errors)?;
// or
let _ = print_summary(passed, warnings, errors);
Key Lesson: Rust forces you to handle errors!
Error 5: Module Not Found
The Error:
error[E0583]: file not found for module `analyze`
|
3 | pub mod analyze;
| ^^^^^^^^^^^^^^^^
What it means:
Declared a module but file doesn't exist
The Fix:
Create the file! Either:
-
src/bin/cli/analyze.rs, OR src/bin/cli/analyze/mod.rs
Key Lesson: Module system can be tricky. Follow the naming conventions!
Learning Resources:
Design Decisions Explained
Why Use Enums for Modifiers?
The Code:
pub enum LocationModifier {
None,
Exact, // =
PrefixPriority, // ^~
Regex, // ~
RegexCaseInsensitive, // ~*
}
Why:
- Type Safety: Can't use invalid modifier
- Match Exhaustiveness: Compiler ensures all cases handled
- Self-Documenting: Clear what options exist
Alternative (Bad):
pub struct Location {
modifier: String, // Could be anything!
}
Why Optional Fields?
The Code:
pub struct Server {
pub root: Option<PathBuf>,
pub locations: Vec<Location>,
}
Why:
-
rootis optional in NGINX -
Optionmakes this explicit - Forces caller to handle None case
Learning Resources:
Why Builder Pattern?
The Code:
let server = Server::new()
.with_server_name("example.com")
.with_listen(listen)
.with_root("/var/www");
Why:
- Fluent API: Readable construction
- Optional Fields: Easy to omit
- Rust Idiom: Common pattern
Implementation:
impl Server {
#[must_use]
pub fn with_server_name(mut self, name: impl Into<String>) -> Self {
self.server_names.push(name.into());
self // Return self for chaining
}
}
Note the #[must_use] attribute - Clippy suggests this!
Why Feature Flags?
Cargo.toml:
[features]
default = ["system"]
system = ["dep:which"]
serde = ["dep:serde", "dep:serde_json"]
cli = ["dep:clap", "dep:colored", "system", "serde"]
Why:
- Optional Dependencies: Users only get what they need
- Smaller Binaries: Exclude unused features
- Library vs Binary: Different needs
Usage:
// In code:
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Server { ... }
The Build Process
Development Workflow
# 1. Check code compiles
cargo check
# 2. Run tests
cargo test
# 3. Run Clippy (linter)
cargo clippy -- -D warnings
# 4. Format code
cargo fmt
# 5. Build release
cargo build --release
# 6. Run benchmarks (if any)
cargo bench
Understanding Cargo.toml
[package]
name = "nginx-discovery"
version = "0.4.0"
edition = "2021" # Rust edition
rust-version = "1.70.0" # Minimum Rust version
[dependencies]
thiserror = "1.0" # Version without ^
serde = { version = "1.0", features = ["derive"], optional = true }
[dev-dependencies] # Only for tests
pretty_assertions = "1.4"
[features]
default = ["system"]
cli = ["dep:clap", "system"]
[[bin]]
name = "nginx-discover"
required-features = ["cli"]
Key Points:
-
edition: Language version -
rust-version: MSRV (minimum supported Rust version) -
optional = true: For feature flags -
dev-dependencies: Not in final binary -
[[bin]]: Binary targets
Build Times
First build (downloads + compiles all deps):
- Debug: ~2-3 minutes
- Release: ~3-5 minutes
Incremental builds (only changed code):
- Debug: ~5-15 seconds
- Release: ~30-60 seconds
Tips to Speed Up Builds:
- Use
cargo check(faster thancargo build) - Use
sccacheormoldlinker - Increase parallel jobs:
cargo build -j 8
Learning Resources
Essential Rust Resources
Books (Free Online):
- The Rust Book - Start here!
- Rust by Example
- The Cargo Book
- Rust Design Patterns
Interactive Learning:
- Rustlings - Small exercises
- Exercism Rust Track
- Rust Playground - Online REPL
Video Resources:
- Let's Get Rusty - YouTube channel
- Jon Gjengset - Advanced Rust
Documentation:
- docs.rs - All crate documentation
- std docs - Standard library
- Rust API Guidelines
Crate-Specific Learning
Clap:
Serde:
Error Handling:
Community Resources
Forums & Chat:
Finding Crates:
- crates.io - Official registry
- lib.rs - Alternative interface
- Blessed.rs - Curated crates
Tips for Learning Rust with AI
1. Ask for Explanations, Not Just Code
Good:
"Why did you use
&strinstead ofStringhere? What's the difference?"
Bad:
"Write me a function"
2. Request Error Analysis
When you get an error:
I got this error: [paste error]
Can you explain what it means and why it happened?
- Ask for Alternatives
You used approach X. What are the alternatives and their trade-offs?
Can you recommend resources for learning about [topic]?
Why did you choose this crate over alternatives?
What are the downsides of this approach?
Is this the idiomatic Rust way to do this?
What would Clippy suggest here?
Let's build this feature step by step. Start with the simplest version.
The code compiles but doesn't work as expected. Let's debug it together.
Common Pitfalls for Beginners
Symptom: Constant borrow checker errors
Solution:
- Learn ownership rules first
- Use
clone()liberally while learning (optimize later) - Read the error messages carefully
2. Over-using Lifetimes
Symptom: Adding 'a everywhere
Solution:
- Most of the time, you don't need explicit lifetimes
- Let the compiler infer them
- Only add when compiler asks
3. Not Reading Compiler Errors
Symptom: Giving up when seeing long errors
Solution:
- Rust errors are helpful! Read them
- Look for the
help:suggestions - Google the error code (E0308, etc.)
4. Avoiding unwrap()
Symptom: unwrap() everywhere
Solution:
- Use
?operator in functions that returnResult - Use
matchorif letto handle errors - Reserve
unwrap()for cases where you know it's safe
5. Not Using Clippy
Symptom: Writing non-idiomatic code
Solution:
- Run
cargo clippyregularly - Fix warnings (they're learning opportunities!)
- Read the explanations at the URLs
Conclusion
Building nginx-discovery was a journey of learning Rust through a real-world project. The key takeaways:
What Worked Well
- AI as Learning Companion - Claude explained errors and design decisions
- Real Project - Much better than toy examples
- Incremental Development - Building feature by feature
- Good Crates - Standing on shoulders of giants
- Testing - Caught bugs early
What Was Challenging
- Borrow Checker - Takes time to internalize
- Type System - More complex than dynamic languages
- Async - Didn't need it, but it's tricky
- Module System - Initial confusion
- Compile Times - Patience required
Advice for Beginners
- Start Small - Don't build nginx-discovery on day 1
- Read Errors - They're your teachers
- Use Clippy - It teaches idiomatic Rust
- Write Tests - They document your learning
- Ask Questions - AI or community, both help
- Build Real Things - Motivation matters
- Be Patient - Rust has a learning curve
What's Next?
Consider building:
- 📊 A simple CLI tool
- 🔍 A log parser
- 🌐 A web scraper
- 📝 A static site generator
- 🎮 A game (with Bevy)
Start with something you care about. That's the best way to learn!
Resources from This Article:
Questions? Comments? Drop them below! I'm happy to help other learners navigate their Rust journey.
Remember: Every Rust expert was once a beginner fighting the borrow checker. You've got this! 🦀
Top comments (0)