Skip to content
portfolio/howtos/Learning Rust/

Chapter 11

Closures & Iterators in Depth

How closures capture their environment, the three Fn traits, iterator internals, and building a real middleware pipeline for our server.

Closure Syntax and Capture Modes

Closures are anonymous functions that can capture variables from their surrounding scope. You've been using them with iterators since Chapter 7. Now let's understand exactly how they work.

// Function - can't access variables from the surrounding scope
fn add_one(x: i32) -> i32 { x + 1 }

// Closure - can capture 'offset' from the environment
let offset = 10;
let add_offset = |x: i32| -> i32 { x + offset };

// Type annotations are optional - the compiler infers them
let add_offset = |x| x + offset;

// Single-expression closures don't need braces
let double = |x| x * 2;

println!("{}", add_offset(5));  // 15
println!("{}", double(5));      // 10

The interesting question is: how does the closure access offset? Rust gives closures three options, and the compiler picks the least restrictive one that works:

Fn - borrow immutably

The closure reads the captured variable but doesn't modify it. The original variable remains usable. This is the most common mode.

let name = String::from("Rust Server");
let greet = || println!("Hello from {}", name);
// 'name' is borrowed by &reference

greet();
greet(); // can call multiple times
println!("{}", name); // 'name' still usable

FnMut - borrow mutably

The closure modifies the captured variable. Only one mutable borrow exists at a time (the usual borrowing rules apply).

let mut count = 0;
let mut increment = || {
    count += 1;
    println!("Count: {}", count);
};
// 'count' is borrowed by &mut reference

increment(); // Count: 1
increment(); // Count: 2
// Can't use 'count' while 'increment' exists (it holds &mut count)

FnOnce - take ownership

The closure moves the captured variable into itself, consuming it. The closure can only be called once because calling it uses up the value.

let name = String::from("connection-1");
let consume = || {
    let owned = name; // moved into the closure
    println!("Consumed: {}", owned);
};
// 'name' has been moved - can't use it anymore

consume(); // fine
// consume(); // ERROR: can't call FnOnce twice
Note
The compiler picks the mode automatically based on what the closure does with the captured variables. If it only reads them: Fn. If it mutates them: FnMut. If it moves them: FnOnce. Every Fn is also a FnMut, and every FnMut is also a FnOnce - they form a hierarchy.

The move Keyword

Sometimes you want a closure to own its captured variables even if it only reads them. The move keyword forces this:

let prefix = String::from("[SERVER]");

// Without move: closure borrows 'prefix'
let log = || println!("{} request received", prefix);

// With move: closure takes ownership of 'prefix'
let log = move || println!("{} request received", prefix);
// 'prefix' is no longer usable here

// This is essential when sending closures to other threads
// (threads need to own their data - Chapter 12)

Closures as Function Parameters and Return Types

Every closure has a unique, anonymous type - the compiler generates a struct for it. You can't name the type directly, so you use trait bounds:

As Parameters

// Accept any closure that takes a &Request and returns bool
fn filter_requests<F>(requests: &[Request], predicate: F) -> Vec<&Request>
where
    F: Fn(&Request) -> bool,
{
    requests.iter().filter(|r| predicate(r)).collect()
}

// Usage:
let gets = filter_requests(&requests, |r| r.method == Method::Get);
let long_paths = filter_requests(&requests, |r| r.path.len() > 20);

Which trait to use in your signature:

TraitUse whenExample
FnYou call the closure multiple times, it doesn't mutatefilter, map, route handlers
FnMutYou call it multiple times, it might mutate statefor_each, fold, sort_by
FnOnceYou only call it onceunwrap_or_else, thread::spawn

Use the most general trait that works: FnOnce accepts the widest range of closures, Fn is the most restrictive but lets you call it repeatedly. If in doubt, start with Fn and relax it if the compiler complains.

As Return Types

Returning closures is trickier because each closure has a unique anonymous type. You have two options:

// Option 1: impl Fn - when you return a single closure type
fn make_greeter(prefix: &str) -> impl Fn(&str) -> String + '_ {
    move |name| format!("{} {}", prefix, name)
}

let greet = make_greeter("Hello");
println!("{}", greet("Rust")); // "Hello Rust"

// Option 2: Box<dyn Fn> - when you might return different closures
fn make_handler(is_admin: bool) -> Box<dyn Fn(&Request) -> Response> {
    if is_admin {
        Box::new(|_req| Response::html("<h1>Admin Panel</h1>"))
    } else {
        Box::new(|_req| Response::html("<h1>Access Denied</h1>"))
    }
}
Tip
Use impl Fn when a function always returns the same kind of closure. Use Box<dyn Fn> when the function might return different closures depending on a condition - the compiler can't know the concrete type at compile time.

Iterator Adaptors and Lazy Evaluation

We covered iterator basics in Chapter 7. Now let's look under the hood. The Iterator trait is surprisingly simple:

trait Iterator {
    type Item;  // Associated type - the type of elements produced

    fn next(&mut self) -> Option<Self::Item>;
    // Returns Some(item) for each element, None when exhausted

    // All other methods (map, filter, etc.) have default
    // implementations built on next()
}

You can implement your own iterator. Let's build one that generates HTTP status codes in a range:

struct StatusRange {
    current: u16,
    end: u16,
}

impl StatusRange {
    fn new(start: u16, end: u16) -> Self {
        StatusRange { current: start, end }
    }
}

impl Iterator for StatusRange {
    type Item = u16;

    fn next(&mut self) -> Option<u16> {
        if self.current <= self.end {
            let code = self.current;
            self.current += 1;
            Some(code)
        } else {
            None
        }
    }
}

// Now it works with all iterator adaptors:
let error_codes: Vec<u16> = StatusRange::new(400, 599)
    .filter(|&c| c == 400 || c == 404 || c == 500)
    .collect();
// [400, 404, 500]

Laziness in Action

Adaptors like map and filter don't do any work immediately. They return a new iterator that wraps the original. Work only happens when a consumer (collect, for_each, count, for loop) pulls values:

let result = (0..1_000_000)
    .map(|i| i * 2)           // no allocation, no work yet
    .filter(|&i| i % 3 == 0)  // still no work
    .take(5)                   // still no work
    .collect::<Vec<_>>();      // NOW it runs - pulls exactly 5 items

// Only ~8 iterations happened, not 1,000,000.
// result: [0, 6, 12, 18, 24]
Note
This is why iterators are as fast as hand-written loops - the compiler fuses the entire chain into a single pass. No intermediate vectors are created. The .take(5) short-circuits the chain after 5 elements, so the million-element range is never fully evaluated.

Useful Patterns for Our Server

use std::collections::HashMap;

// Parse query string: "name=rust&version=1" → HashMap
fn parse_query(query: &str) -> HashMap<String, String> {
    query.split('&')
        .filter_map(|pair| {
            let (key, value) = pair.split_once('=')?;
            Some((key.to_string(), value.to_string()))
        })
        .collect()
}

// Split path into segments: "/api/users/42" → ["api", "users", "42"]
fn path_segments(path: &str) -> Vec<&str> {
    path.split('/')
        .filter(|s| !s.is_empty())
        .collect()
}

// Check if all required headers are present
fn has_required_headers(headers: &HashMap<String, String>, required: &[&str]) -> bool {
    required.iter().all(|name| headers.contains_key(*name))
}

// Collect missing headers
fn missing_headers<'a>(
    headers: &HashMap<String, String>,
    required: &'a [&'a str],
) -> Vec<&'a str> {
    required.iter()
        .filter(|&&name| !headers.contains_key(name))
        .copied()
        .collect()
}

Building a Middleware Pipeline with Closures

Middleware is a function that wraps a handler - it runs before and/or after the handler, adding cross-cutting behavior like logging, timing, or authentication. Closures make this elegant because each middleware captures the next handler in the chain.

The pattern: a middleware is a function that takes a handler and returns a new handler:

use crate::error::ServerError;
use crate::request::Request;
use crate::response::Response;

/// A handler is anything that takes a Request and returns a Response.
type BoxHandler = Box<dyn Fn(&Request) -> Result<Response, ServerError>>;

/// A middleware wraps a handler, returning a new handler.
type Middleware = fn(BoxHandler) -> BoxHandler;

Logging Middleware

fn logging(next: BoxHandler) -> BoxHandler {
    Box::new(move |req: &Request| {
        println!("--> {} {}", req.method, req.path);
        let result = next(req);
        match &result {
            Ok(resp) => println!("<-- {} {}", resp.status, req.path),
            Err(e) => println!("<-- ERROR {}", e),
        }
        result
    })
}

The outer function takes the next handler. It returns a closure that captures next with move. When called, the closure logs the request, calls the inner handler, logs the result, and returns it. The handler is wrapped - not modified.

Timing Middleware

use std::time::Instant;

fn timing(next: BoxHandler) -> BoxHandler {
    Box::new(move |req: &Request| {
        let start = Instant::now();
        let result = next(req);
        let elapsed = start.elapsed();
        println!("  {} {} took {:?}", req.method, req.path, elapsed);
        result
    })
}

Default Headers Middleware

fn default_headers(next: BoxHandler) -> BoxHandler {
    Box::new(move |req: &Request| {
        let mut resp = next(req)?;
        resp.add_header("Server", "rust-webserver/0.1");
        resp.add_header("X-Powered-By", "Rust");
        Ok(resp)
    })
}

Composing the Pipeline

Middleware composes by wrapping - each layer wraps the one inside it. The outermost middleware runs first on the request and last on the response:

/// Apply a stack of middleware to a handler.
fn apply_middleware(handler: BoxHandler, middleware: Vec<Middleware>) -> BoxHandler {
    middleware.into_iter()
        .rev()           // Apply inner-to-outer so execution is outer-to-inner
        .fold(handler, |h, mw| mw(h))
}

// Build the pipeline
let handler: BoxHandler = Box::new(|req: &Request| {
    match req.path.as_str() {
        "/" => Ok(Response::html("<h1>Home</h1>")),
        _ => Err(ServerError::NotFound(req.path.clone())),
    }
});

let pipeline = apply_middleware(handler, vec![
    logging,          // outermost - runs first
    timing,           // middle
    default_headers,  // innermost - runs last (closest to handler)
]);

// A request flows through:
// logging → timing → default_headers → handler → default_headers → timing → logging

The fold is the key - it chains everything together. Starting with the handler, it wraps each middleware around it from inner to outer (hence the .rev()). The result is a single BoxHandler that can be called like any other.

Tip
This is the same pattern that Express.js, Koa, and Tower (Rust's production middleware library) use. Middleware wraps middleware wraps the handler. Each layer has a chance to inspect/modify the request on the way in and the response on the way out.

Chaining Request Transformations

Closures also work for transforming requests before they reach the handler - normalizing paths, parsing query strings, or authenticating. Here's a pattern using iterator-style chaining:

/// Normalize a request path: remove trailing slashes, lowercase.
fn normalize_path(path: &str) -> String {
    let trimmed = path.trim_end_matches('/');
    let normalized = if trimmed.is_empty() { "/" } else { trimmed };
    normalized.to_lowercase()
}

/// Extract path and query: "/search?q=rust" → ("/search", Some("q=rust"))
fn split_path_query(path: &str) -> (&str, Option<&str>) {
    match path.split_once('?') {
        Some((path, query)) => (path, Some(query)),
        None => (path, None),
    }
}

/// A request pipeline that normalizes before routing.
fn process(req: &Request) -> Result<Response, ServerError> {
    let (path, query) = split_path_query(&req.path);
    let normalized = normalize_path(path);

    // Parse query params if present
    let params: HashMap<String, String> = query
        .map(parse_query)
        .unwrap_or_default();

    // Route based on normalized path
    match normalized.as_str() {
        "/" => Ok(Response::html("<h1>Home</h1>")),
        "/search" => {
            let q = params.get("q").map(|s| s.as_str()).unwrap_or("*");
            Ok(Response::html(&format!("<h1>Search: {}</h1>", q)))
        }
        _ => Err(ServerError::NotFound(normalized)),
    }
}

The transformation chain is: raw path → split query → normalize → parse query params → route. Each step is a small function, and they compose cleanly. The Option and iterator methods (map, unwrap_or_default) handle the "query might not exist" case without any if statements.

Building a Transform Pipeline

For more complex scenarios, you can chain transformations as a list of closures:

type Transform = Box<dyn Fn(String) -> String>;

fn build_path_pipeline() -> Vec<Transform> {
    vec![
        Box::new(|path| path.trim_end_matches('/').to_string()),
        Box::new(|path| if path.is_empty() { "/".to_string() } else { path }),
        Box::new(|path| path.to_lowercase()),
    ]
}

fn apply_transforms(path: &str, transforms: &[Transform]) -> String {
    transforms.iter().fold(path.to_string(), |p, t| t(p))
}

let pipeline = build_path_pipeline();
assert_eq!(apply_transforms("/API/Users/", &pipeline), "/api/users");
assert_eq!(apply_transforms("/", &pipeline), "/");
assert_eq!(apply_transforms("///", &pipeline), "/");
Note
fold is the functional programming equivalent of a loop with an accumulator. It starts with an initial value, applies each closure in turn, and returns the final result. It's the foundation of both our middleware and transform pipelines.

Exercise

  1. Write an auth middleware that checks for an Authorization header. If missing, return a 401 Unauthorized response without calling the inner handler. If present, call the inner handler normally.
  2. Implement a custom iterator ChunkedBody that takes a &str body and a chunk size, and yields &str slices of that size. Use it to simulate chunked transfer encoding.
  3. Write a rate_limit middleware that uses a captured FnMut counter. After 10 calls, it should return 429 Too Many Requests. What capture mode does this require? Why can't it be Fn?
  4. Create a function fn compose<F, G>(f: F, g: G) -> impl Fn(String) -> String that takes two string transformations and returns a new closure applying f then g. Use it to combine to_lowercase and trim.
  5. Rewrite parse_query to also handle URL-decoded values: name=hello%20world should produce ("name", "hello world"). Write a url_decode function and use .map() in the iterator chain.

What's Next

You now understand closures deeply - how they capture variables, which Fn trait applies, and how to pass them around. Combined with iterators, they give you a powerful toolkit for building composable middleware, transform pipelines, and data processing chains.

But our server still handles one connection at a time. In the next chapter, we tackle concurrency - threads, shared state with Arc<Mutex<T>>, channels, and building a thread pool so our server can handle many connections simultaneously.