DEV Community

Ajit Kumar
Ajit Kumar

Posted on

Learning Rust by Building nginx-discovery: A Beginner's Journey with AI

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

  1. The Project Overview
  2. Understanding the Crate Ecosystem
  3. Common Rust Errors and How to Fix Them
  4. Design Decisions Explained
  5. The Build Process
  6. Learning Resources
  7. 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)
Enter fullscreen mode Exit fullscreen mode

Why This Project is Great for Learning

  1. Real-world problem - Actual DevOps use case
  2. Multiple concepts - Parsing, CLI, data structures
  3. Error handling - Learn Result properly
  4. Type system - See Rust's types in action
  5. Ecosystem - Use popular crates
  6. 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),
}
Enter fullscreen mode Exit fullscreen mode

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)?;
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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()?;
Enter fullscreen mode Exit fullscreen mode

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")?
        // ...
}
Enter fullscreen mode Exit fullscreen mode

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 nginx binary
  • 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"),
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 {
    |     ^^^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

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() {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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<()> {
                  ^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

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);
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
   | ^^^^^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

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, // ~*
}
Enter fullscreen mode Exit fullscreen mode

Why:

  1. Type Safety: Can't use invalid modifier
  2. Match Exhaustiveness: Compiler ensures all cases handled
  3. Self-Documenting: Clear what options exist

Alternative (Bad):

pub struct Location {
    modifier: String,  // Could be anything!
}
Enter fullscreen mode Exit fullscreen mode

Why Optional Fields?

The Code:

pub struct Server {
    pub root: Option<PathBuf>,
    pub locations: Vec<Location>,
}
Enter fullscreen mode Exit fullscreen mode

Why:

  • root is optional in NGINX
  • Option makes 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");
Enter fullscreen mode Exit fullscreen mode

Why:

  1. Fluent API: Readable construction
  2. Optional Fields: Easy to omit
  3. 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Why:

  1. Optional Dependencies: Users only get what they need
  2. Smaller Binaries: Exclude unused features
  3. Library vs Binary: Different needs

Usage:

// In code:
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};

#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Server { ... }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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:

  1. Use cargo check (faster than cargo build)
  2. Use sccache or mold linker
  3. Increase parallel jobs: cargo build -j 8

Learning Resources

Essential Rust Resources

Books (Free Online):

  1. The Rust Book - Start here!
  2. Rust by Example
  3. The Cargo Book
  4. Rust Design Patterns

Interactive Learning:

  1. Rustlings - Small exercises
  2. Exercism Rust Track
  3. Rust Playground - Online REPL

Video Resources:

  1. Let's Get Rusty - YouTube channel
  2. Jon Gjengset - Advanced Rust

Documentation:

  1. docs.rs - All crate documentation
  2. std docs - Standard library
  3. Rust API Guidelines

Crate-Specific Learning

Clap:

Serde:

Error Handling:

Community Resources

Forums & Chat:

Finding Crates:

Tips for Learning Rust with AI

1. Ask for Explanations, Not Just Code

Good:

"Why did you use &str instead of String here? 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?
Enter fullscreen mode Exit fullscreen mode



  1. Ask for Alternatives

You used approach X. What are the alternatives and their trade-offs?
Enter fullscreen mode Exit fullscreen mode

  • Request Learning Resources
    Can you recommend resources for learning about [topic]?
    
    Enter fullscreen mode Exit fullscreen mode

  • Challenge Decisions
    Why did you choose this crate over alternatives?
    What are the downsides of this approach?
    
    Enter fullscreen mode Exit fullscreen mode

  • Ask for Best Practices
    Is this the idiomatic Rust way to do this?
    What would Clippy suggest here?
    
    Enter fullscreen mode Exit fullscreen mode

  • Request Incremental Learning
    Let's build this feature step by step. Start with the simplest version.
    
    Enter fullscreen mode Exit fullscreen mode

  • Debug Together
    The code compiles but doesn't work as expected. Let's debug it together.
    
    Enter fullscreen mode Exit fullscreen mode

    Common Pitfalls for Beginners

  • Fighting the Borrow Checker
  • 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 return Result
    • Use match or if let to handle errors
    • Reserve unwrap() for cases where you know it's safe

    5. Not Using Clippy

    Symptom: Writing non-idiomatic code

    Solution:

    • Run cargo clippy regularly
    • 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

    1. AI as Learning Companion - Claude explained errors and design decisions
    2. Real Project - Much better than toy examples
    3. Incremental Development - Building feature by feature
    4. Good Crates - Standing on shoulders of giants
    5. Testing - Caught bugs early

    What Was Challenging

    1. Borrow Checker - Takes time to internalize
    2. Type System - More complex than dynamic languages
    3. Async - Didn't need it, but it's tricky
    4. Module System - Initial confusion
    5. Compile Times - Patience required

    Advice for Beginners

    1. Start Small - Don't build nginx-discovery on day 1
    2. Read Errors - They're your teachers
    3. Use Clippy - It teaches idiomatic Rust
    4. Write Tests - They document your learning
    5. Ask Questions - AI or community, both help
    6. Build Real Things - Motivation matters
    7. 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)