Skip to content
portfolio/howtos/Learning Rust/

Chapter 07

Collections & the Standard Library

Vectors, HashMaps, iterators, and essential traits - the tools that replace verbose loops with concise, expressive code.

Vectors - Dynamic Arrays

We've been using Vec since Chapter 4 without fully explaining it. A Vec<T> is a growable, heap-allocated array. It's the collection you'll reach for most often - our Request stores headers as a Vec<(String, String)>, and our Response::to_bytes returns Vec<u8>.

// Create with vec! macro
let methods = vec!["GET", "POST", "PUT", "DELETE"];

// Or create empty and push
let mut headers: Vec<(String, String)> = Vec::new();
headers.push(("Host".to_string(), "localhost".to_string()));
headers.push(("Accept".to_string(), "text/html".to_string()));

// Access by index (panics if out of bounds)
let first = &methods[0]; // "GET"

// Safe access with .get() - returns Option<&T>
let maybe = methods.get(10); // None

// Length and emptiness
println!("{} headers", headers.len());  // 2
println!("empty? {}", headers.is_empty()); // false

Common Operations

let mut codes = vec![200, 404, 500];

// Add and remove
codes.push(301);           // [200, 404, 500, 301]
let last = codes.pop();    // Some(301), vec is [200, 404, 500]
codes.insert(1, 201);      // [200, 201, 404, 500]
codes.remove(2);           // [200, 201, 500] - removes 404

// Check membership
let has_200 = codes.contains(&200); // true

// Sort
codes.sort();               // [200, 201, 500]

// Iterate
for code in &codes {
    println!("{}", code);
}

// Retain only elements matching a condition
codes.retain(|&c| c < 300); // [200, 201]
Note
&codes borrows the vector immutably - you can read but not modify it. If you write for code in codes (without &), it moves the vector into the loop and you can't use it after. For mutable iteration, use for code in &mut codes.

Slices

A slice (&[T]) is a reference to a contiguous sequence of elements in a Vec or array. Slices are to Vec what &str is to String - a borrowed view:

let codes = vec![200, 301, 404, 500];

let all: &[i32] = &codes;          // slice of everything
let errors: &[i32] = &codes[2..];  // [404, 500]
let middle: &[i32] = &codes[1..3]; // [301, 404]

// Functions should take &[T] to accept both Vec and array
fn sum_codes(codes: &[u16]) -> u16 {
    let mut total = 0;
    for code in codes {
        total += code;
    }
    total
}

let from_vec = sum_codes(&codes);           // works
let from_array = sum_codes(&[200, 404]);    // also works
Tip
Prefer &[T] in function signatures over &Vec<T>. A slice accepts any contiguous sequence - vectors, arrays, other slices. It's more flexible, just like preferring &str over &String.

HashMaps - Key-Value Storage

A HashMap<K, V> stores key-value pairs with O(1) lookup. It's not in the prelude, so you need to import it:

use std::collections::HashMap;

// Create and insert
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "text/html".to_string());
headers.insert("Server".to_string(), "rust-webserver".to_string());

// Look up by key - returns Option<&V>
if let Some(ct) = headers.get("Content-Type") {
    println!("Content-Type: {}", ct);
}

// Default if missing
let server = headers.get("Server").unwrap_or(&"unknown".to_string());

// Check if key exists
let has_auth = headers.contains_key("Authorization"); // false

// Remove a key
headers.remove("Server");

// Iterate
for (key, value) in &headers {
    println!("{}: {}", key, value);
}

The Entry API

The entry API lets you insert or update in a single step without double-lookups:

use std::collections::HashMap;

let mut visit_counts: HashMap<String, u32> = HashMap::new();

// If the key doesn't exist, insert 0, then add 1
*visit_counts.entry("/".to_string()).or_insert(0) += 1;
*visit_counts.entry("/".to_string()).or_insert(0) += 1;
*visit_counts.entry("/about".to_string()).or_insert(0) += 1;

// {"/": 2, "/about": 1}
for (path, count) in &visit_counts {
    println!("{}: {} visits", path, count);
}

entry() returns an Entry enum - either Occupied (key exists) or Vacant (it doesn't). or_insert(0) inserts 0 if vacant, then returns a mutable reference to the value either way. We dereference with * and increment. This pattern is ideal for counting - like tracking request counts per path.

Note
HashMap does not maintain insertion order. If you need ordered keys, use BTreeMap from the same module. For HTTP headers where order sometimes matters, a Vec<(String, String)> preserves order (which is why we used that in Chapter 6).

Iterators, map, filter, collect

Iterators are Rust's way of processing sequences. Every collection has an .iter() method that produces an iterator. You chain adaptors like map and filter to transform data, then call a consumer like collect or for_each to produce a result.

Iterators are lazy - nothing runs until a consumer pulls values. This means chaining three adaptors doesn't create three intermediate collections.

map - Transform Each Element

let paths = vec!["/home", "/about", "/api/users"];

// Uppercase each path
let upper: Vec<String> = paths
    .iter()
    .map(|p| p.to_uppercase())
    .collect();
// ["/HOME", "/ABOUT", "/API/USERS"]

The |p| is a closure - an inline anonymous function. We'll cover closures in depth in Chapter 11. For now, read |p| p.to_uppercase() as "take each element p and return p.to_uppercase()."

filter - Keep Only What Matches

let codes = vec![200, 301, 404, 200, 500, 200];

let errors: Vec<&u16> = codes
    .iter()
    .filter(|&&c| c >= 400)
    .collect();
// [&404, &500]

// Count instead of collecting
let error_count = codes.iter().filter(|&&c| c >= 400).count();
// 2

Chaining Adaptors

The real power is in chaining. Here's a realistic example - parsing raw header lines into key-value pairs:

let raw_headers = "Host: localhost\r\nAccept: text/html\r\nX-Custom: hello";

let headers: Vec<(String, String)> = raw_headers
    .split("\r\n")
    .filter_map(|line| {
        let (key, value) = line.split_once(':')?;
        Some((key.trim().to_string(), value.trim().to_string()))
    })
    .collect();

// [("Host", "localhost"), ("Accept", "text/html"), ("X-Custom", "hello")]

filter_map combines filter and map - the closure returns Option<T>. Some values are kept and unwrapped, None values are discarded. It's perfect when a transformation might fail for some elements.

Other Useful Adaptors

let items = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// take / skip
let first_three: Vec<&i32> = items.iter().take(3).collect();  // [1, 2, 3]
let after_five: Vec<&i32> = items.iter().skip(5).collect();   // [6, 7, 8, 9, 10]

// enumerate - get index alongside value
for (i, item) in items.iter().enumerate() {
    println!("[{}] = {}", i, item);
}

// find - first element matching a predicate
let first_even = items.iter().find(|&&x| x % 2 == 0); // Some(&2)

// any / all - boolean checks
let has_negative = items.iter().any(|&x| x < 0);    // false
let all_positive = items.iter().all(|&x| x > 0);    // true

// fold - accumulate into a single value
let sum = items.iter().fold(0, |acc, &x| acc + x);   // 55

// join strings (via collect into String)
let methods = vec!["GET", "POST", "PUT"];
let joined = methods.join(", "); // "GET, POST, PUT"
Tip
When you see iterator chains, read them top to bottom like a pipeline: "start with this, then filter, then transform, then collect." Each step feeds into the next. The compiler optimizes this into a single pass - it's as fast as a hand-written loop.

Useful Standard Library Traits

Traits define shared behavior. The standard library has several traits you'll use constantly. Implementing them makes your types work with the rest of the ecosystem - printing, converting, comparing.

Debug - Developer-Facing Output

// Derive it - almost always the right choice
#[derive(Debug)]
struct Request {
    method: String,
    path: String,
}

let req = Request { method: "GET".into(), path: "/".into() };
println!("{:?}", req);   // Request { method: "GET", path: "/" }
println!("{:#?}", req);  // pretty-printed with newlines

Display - User-Facing Output

Debug is for developers ({:?}). Display is for users ({}). Display can't be derived - you implement it manually because how something looks to a user is a design decision:

use std::fmt;

enum Method {
    Get,
    Post,
    Unknown(String),
}

impl fmt::Display for Method {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Method::Get => write!(f, "GET"),
            Method::Post => write!(f, "POST"),
            Method::Unknown(m) => write!(f, "{}", m),
        }
    }
}

let method = Method::Get;
println!("Method: {}", method);        // "Method: GET"
let s = method.to_string();            // Display gives you .to_string() for free
println!("As string: {}", s);
Note
Implementing Display automatically gives your type a .to_string() method via the ToString trait. You never need to implement ToString directly.

From and Into - Type Conversions

From<T> defines how to create a type from another type. Its mirror, Into<T>, is provided automatically when you implement From:

struct StatusCode(u16);

impl From<u16> for StatusCode {
    fn from(code: u16) -> Self {
        StatusCode(code)
    }
}

// Now you can convert both ways:
let status = StatusCode::from(200);   // explicit
let status: StatusCode = 404.into();  // via Into (provided free)

// From<&str> for String is why this works:
let s: String = "hello".into();
let s = String::from("hello");        // same thing

From is everywhere in Rust. It's how String::from("...") works, how error types convert, and how the ? operator chains errors (as we'll see in Chapter 8).

Applying Collections: A Routing Table

Let's apply everything we've learned to improve our webserver. In Chapter 6, routing was a big nested match. With a HashMap, we can build a data-driven routing table that's easier to extend:

src/main.rs
use std::collections::HashMap;
use std::fmt;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};

// --- Types ---

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Method {
    Get,
    Post,
}

impl Method {
    fn parse(s: &str) -> Option<Method> {
        match s {
            "GET" => Some(Method::Get),
            "POST" => Some(Method::Post),
            _ => None,
        }
    }
}

impl fmt::Display for Method {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Method::Get => write!(f, "GET"),
            Method::Post => write!(f, "POST"),
        }
    }
}

struct Request {
    method: Method,
    path: String,
    headers: HashMap<String, String>,
}

impl Request {
    fn parse(raw: &str) -> Option<Request> {
        let mut lines = raw.split("\r\n");

        let request_line = lines.next()?;
        let mut parts = request_line.split_whitespace();
        let method = Method::parse(parts.next()?)?;
        let path = parts.next()?.to_string();

        // Parse headers into a HashMap for O(1) lookup
        let headers: HashMap<String, String> = lines
            .take_while(|line| !line.is_empty())
            .filter_map(|line| {
                let (key, value) = line.split_once(':')?;
                Some((
                    key.trim().to_lowercase(),
                    value.trim().to_string(),
                ))
            })
            .collect();

        Some(Request { method, path, headers })
    }
}

struct Response {
    status: u16,
    reason: &'static str,
    headers: Vec<(String, String)>,
    body: String,
}

impl Response {
    fn new(status: u16, reason: &'static str, body: &str) -> Self {
        Response {
            status,
            reason,
            headers: vec![],
            body: body.to_string(),
        }
    }

    fn html(status: u16, reason: &'static str, body: &str) -> Self {
        let mut resp = Self::new(status, reason, body);
        resp.headers.push((
            "Content-Type".to_string(),
            "text/html; charset=utf-8".to_string(),
        ));
        resp
    }

    fn to_bytes(&self) -> Vec<u8> {
        // Build headers with iterators
        let header_lines: String = self.headers
            .iter()
            .map(|(k, v)| format!("{}: {}", k, v))
            .collect::<Vec<_>>()
            .join("\r\n");

        let mut output = format!(
            "HTTP/1.1 {} {}\r\nContent-Length: {}\r\nConnection: close\r\n",
            self.status, self.reason, self.body.len()
        );
        if !header_lines.is_empty() {
            output.push_str(&header_lines);
            output.push_str("\r\n");
        }
        output.push_str("\r\n");
        output.push_str(&self.body);
        output.into_bytes()
    }
}

// --- Route handler type ---

type Handler = fn(&Request) -> Response;

// --- Handlers ---

fn home(_req: &Request) -> Response {
    Response::html(200, "OK", "<h1>Welcome!</h1><p><a href=\"/about\">About</a></p>")
}

fn about(_req: &Request) -> Response {
    Response::html(200, "OK", "<h1>About</h1><p>Built with Rust.</p>")
}

fn headers_page(req: &Request) -> Response {
    // Use iterators to build the HTML list
    let items: String = req.headers
        .iter()
        .map(|(k, v)| format!("<li><b>{}:</b> {}</li>", k, v))
        .collect::<Vec<_>>()
        .join("\n");

    let body = format!(
        "<h1>Request Headers</h1><ul>{}</ul><p><a href=\"/\">Home</a></p>",
        items
    );
    Response::html(200, "OK", &body)
}

// --- Router ---

struct Router {
    routes: HashMap<(Method, String), Handler>,
}

impl Router {
    fn new() -> Self {
        Router { routes: HashMap::new() }
    }

    fn add(&mut self, method: Method, path: &str, handler: Handler) {
        self.routes.insert((method, path.to_string()), handler);
    }

    fn route(&self, request: &Request) -> Response {
        let key = (request.method.clone(), request.path.clone());
        match self.routes.get(&key) {
            Some(handler) => handler(request),
            None => Response::html(
                404, "Not Found",
                &format!("<h1>404</h1><p>{} {} not found</p>", request.method, request.path),
            ),
        }
    }
}

// --- Main ---

fn handle_connection(stream: &mut TcpStream, router: &Router) {
    let mut buffer = [0u8; 4096];
    let bytes_read = match stream.read(&mut buffer) {
        Ok(0) => return,
        Ok(n) => n,
        Err(_) => return,
    };

    let raw = String::from_utf8_lossy(&buffer[..bytes_read]);
    let response = match Request::parse(&raw) {
        Some(req) => {
            println!("{} {}", req.method, req.path);
            router.route(&req)
        }
        None => Response::new(400, "Bad Request", "Malformed request"),
    };

    let _ = stream.write_all(&response.to_bytes());
    let _ = stream.flush();
}

fn main() {
    // Build the routing table
    let mut router = Router::new();
    router.add(Method::Get, "/", home);
    router.add(Method::Get, "/index.html", home);
    router.add(Method::Get, "/about", about);
    router.add(Method::Get, "/headers", headers_page);

    let addr = "127.0.0.1:8080";
    let listener = TcpListener::bind(addr).unwrap();
    println!("Listening on http://{}", addr);

    for stream in listener.incoming() {
        if let Ok(mut stream) = stream {
            handle_connection(&mut stream, &router);
        }
    }
}

Let's highlight what changed from Chapter 6:

  • Headers are a HashMap - O(1) lookup instead of linear search. We lowercase keys during parsing for case-insensitive matching.
  • Header parsing uses iterators - take_while, filter_map, and collect replace the manual loop.
  • Router is a HashMap - routes are data, not code. Adding a new route is one line: router.add(Method::Get, "/new", handler_fn).
  • Handler is a type alias - type Handler = fn(&Request) -> Response is a function pointer. Each route points to a function.
  • Response headers use iterators to serialize - map + join instead of a manual loop.
  • Method implements Display - so we can print it with {} in format strings and error pages.
Note
To use Method as a HashMap key, it needs #[derive(Hash, Eq, PartialEq)]. HashMap keys must be hashable and comparable. We also added Clone so we can clone the method when constructing the lookup key.

Exercise

  1. Add a request counter using HashMap's entry API. Track how many times each path has been requested and add a /stats page that displays the counts. (Hint: you'll need the router to accept &mut state - or pass the counter separately.)
  2. Rewrite Request::parse to return Result<Request, String> instead of Option<Request>, with descriptive error messages like "missing request line" or "invalid method."
  3. Use iter().any() to write a function has_header(req: &Request, name: &str) -> bool that checks if a header exists. Then use iter().find() to get the first header matching a prefix (like all headers starting with "X-").
  4. Implement Display for Response that prints a log-friendly summary like "200 OK (text/html, 142 bytes)".
  5. Add Put and Delete to Method, including Display, Hash, and parse. Register a DELETE /clear route that responds with "Cleared."

What's Next

You now have the major collection types and iterator patterns that make Rust code concise. The server uses HashMap for both header storage and routing, iterators for parsing and serialization, and standard traits for printing and conversion.

But we're still using .unwrap() in too many places. In the next chapter, we'll tackle error handling done right - the ? operator, custom error types, and returning proper HTTP error responses when things go wrong.