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)); // 10The 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 usableFnMut - 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 twiceFn. 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:
| Trait | Use when | Example |
|---|---|---|
| Fn | You call the closure multiple times, it doesn't mutate | filter, map, route handlers |
| FnMut | You call it multiple times, it might mutate state | for_each, fold, sort_by |
| FnOnce | You only call it once | unwrap_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>"))
}
}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].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 → loggingThe 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.
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), "/");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
- Write an
authmiddleware that checks for anAuthorizationheader. If missing, return a401 Unauthorizedresponse without calling the inner handler. If present, call the inner handler normally. - Implement a custom iterator
ChunkedBodythat takes a&strbody and a chunk size, and yields&strslices of that size. Use it to simulate chunked transfer encoding. - Write a
rate_limitmiddleware that uses a capturedFnMutcounter. After 10 calls, it should return429 Too Many Requests. What capture mode does this require? Why can't it beFn? - Create a function
fn compose<F, G>(f: F, g: G) -> impl Fn(String) -> Stringthat takes two string transformations and returns a new closure applyingftheng. Use it to combineto_lowercaseandtrim. - Rewrite
parse_queryto also handle URL-decoded values:name=hello%20worldshould produce("name", "hello world"). Write aurl_decodefunction 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.