12 Prompts for Rust: Safe Systems Programming, CLI Tools, and WebAssembly

Introduction

Rust has been Stack Overflow's most loved language for nine consecutive years (2016–2024) and is now used by 85% of Fortune 500 companies for performance-critical systems (source: Rust Foundation 2025 survey). Whether you're building a zero-cost abstraction CLI utility, a memory-safe system service, or compiling Rust to WebAssembly for the browser, the right prompt can cut your development time in half.

This collection of 12 prompts is designed for Rust developers who want to move from “it compiles” to “it’s production-ready.” You’ll find practical, battle-tested prompts for safe concurrency, error handling, CLI argument parsing, WebAssembly interop, and even embedded Rust patterns. Each prompt includes a realistic scenario, the exact prompt text, and a code example you can adapt.

1. Safe Concurrency with std::sync::Arc and Mutex

Scenario: You have a shared counter that multiple threads increment. Without proper synchronization, you get data races. Rust’s type system prevents undefined behavior, but you need a correct pattern.

Prompt:

"Generate a Rust function that creates a shared mutable counter using Arc<Mutex<u64>>. Spawn 10 threads, each incrementing the counter 1,000 times. After all threads join, print the final count. Use std::sync::mpsc for logging each increment in a separate thread."

Example Result:

use std::sync::{Arc, Mutex, mpsc};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0u64));
    let (tx, rx) = mpsc::channel();
    let mut handles = vec![];

    for i in 0..10 {
        let counter = Arc::clone(&counter);
        let tx = tx.clone();
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                let mut val = counter.lock().unwrap();
                *val += 1;
                tx.send(format!("Thread {} incremented to {}", i, *val)).unwrap();
            }
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }
    drop(tx);

    for msg in rx {
        println!("{}", msg);
    }
    println!("Final counter: {}", *counter.lock().unwrap());
}

Key takeaway: Arc<Mutex<T>> is the standard approach for shared mutable state across threads. The prompt forces you to combine two sync primitives safely.

2. Zero-Cost CLI Argument Parsing with clap

Scenario: You need a CLI tool that accepts a file path (-f), an optional output format (--format), and a verbosity flag (-v). Hand-parsing is error-prone.

Prompt:

"Write a Rust CLI using clap v4 (derive macro) that accepts -f <path>, --format <json|yaml> (default json), and -v for verbose mode. Print parsed values. Use clap::Parser derive."

Example Result:

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about = "My CLI tool")]
struct Args {
    /// Input file path
    #[arg(short = 'f', long = "file", required = true)]
    file: String,

    /// Output format
    #[arg(long = "format", default_value = "json")]
    format: String,

    /// Verbose mode
    #[arg(short, long)]
    verbose: bool,
}

fn main() {
    let args = Args::parse();
    println!("File: {}", args.file);
    println!("Format: {}", args.format);
    if args.verbose {
        println!("Verbose mode on");
    }
}

Key takeaway: clap’s derive API gives you zero-cost abstraction—no runtime overhead for parsing. The prompt ensures you cover required vs optional arguments.

3. Compile Rust to WebAssembly with wasm-pack

Scenario: You have a Rust library that performs heavy numeric computations. You want to call it from JavaScript in the browser.

Prompt:

"Create a Rust function fibonacci(n: u32) -> u64 that computes the nth Fibonacci number using an iterative approach. Export it to WebAssembly using wasm-bindgen. Write a JavaScript snippet that calls this function and logs the result."

Example Result:

// lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let mut a = 0;
            let mut b = 1;
            for _ in 2..=n {
                let temp = a + b;
                a = b;
                b = temp;
            }
            b
        }
    }
}
// index.js
import init, { fibonacci } from './pkg/my_wasm.js';

async function run() {
    await init();
    console.log(fibonacci(30)); // 832040
}
run();

Key takeaway: wasm-bindgen handles serialization and memory management automatically. The prompt forces you to think about the interface boundary: only Copy types or JsValue can cross the bridge.

4. Error Handling with thiserror and anyhow

Scenario: Your application has multiple error types (IO, parsing, network). You need a unified error type for main() that preserves context.

Prompt:

"Define a custom error enum using thiserror for a file-reading application that can fail with IoError, ParseError(String), and NetworkError. Then write a function read_config(path: &str) -> Result<String, AppError> that opens a file and returns its content, using ? to propagate errors. Use anyhow::Result in main()."

Example Result:

use thiserror::Error;
use std::fs::File;
use std::io::Read;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Parse error: {0}")]
    Parse(String),
    #[error("Network error: {0}")]
    Network(String),
}

fn read_config(path: &str) -> Result<String, AppError> {
    let mut file = File::open(path)?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}

fn main() -> anyhow::Result<()> {
    let config = read_config("config.toml")?;
    println!("{}", config);
    Ok(())
}

Key takeaway: thiserror reduces boilerplate for library errors; anyhow is for application-level error handling where you just want to ? and print.

5. Parsing JSON with serde_json and Struct Derivation

Scenario: You have a JSON API response with nested fields. You need to deserialize it into a Rust struct.

Prompt:

"Given the JSON: {\"user\":{\"id\":42,\"name\":\"Alice\"},\"status\":\"ok\"}, define a Rust struct using serde::Deserialize that captures the full structure. Write a function parse_data(json: &str) -> Result<Response, serde_json::Error> that deserializes and prints the user's name."

Example Result:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct User {
    id: u32,
    name: String,
}

#[derive(Deserialize, Debug)]
struct Response {
    user: User,
    status: String,
}

fn parse_data(json: &str) -> Result<Response, serde_json::Error> {
    let resp: Response = serde_json::from_str(json)?;
    println!("User name: {}", resp.user.name);
    Ok(resp)
}

fn main() {
    let data = r#"{"user":{"id":42,"name":"Alice"},"status":"ok"}"#;
    parse_data(data).unwrap();
}

Key takeaway: serde is the de facto standard for serialization in Rust. The prompt teaches nested struct derivation and error handling with ?.

6. Building a TCP Server with std::net

Scenario: You need a simple echo server that accepts connections and sends back whatever it receives.

Prompt:

"Write a Rust TCP server using std::net::TcpListener that listens on 127.0.0.1:8080. For each connection, spawn a new thread that reads data in chunks and writes it back. Use BufRead for line-based reading. Handle graceful shutdown on Ctrl+C."

Example Result:

use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

fn handle_client(mut stream: TcpStream) {
    let peer = stream.peer_addr().unwrap();
    println!("New connection: {}", peer);
    let mut reader = BufReader::new(stream.try_clone().unwrap());
    for line in reader.lines() {
        match line {
            Ok(msg) => {
                println!("Received: {}", msg);
                if let Err(e) = writeln!(stream, "Echo: {}", msg) {
                    eprintln!("Write error: {}", e);
                    break;
                }
            }
            Err(e) => {
                eprintln!("Read error: {}", e);
                break;
            }
        }
    }
}

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080")?;
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                thread::spawn(|| handle_client(stream));
            }
            Err(e) => eprintln!("Connection failed: {}", e),
        }
    }
    Ok(())
}

Key takeaway: Rust’s standard library provides low-level networking without external crates. The prompt introduces concurrency patterns and BufRead for efficient I/O.

7. Writing a Custom Iterator

Scenario: You have a sequence of Fibonacci numbers, but you want to generate them lazily without allocating a vector.

Prompt:

"Implement a Fibonacci struct that implements Iterator yielding u64 values. The iterator should never overflow—use Option<u64> and return None when the next value exceeds u64::MAX. Test it by taking the first 10 values."

Example Result:

struct Fibonacci {
    a: u64,
    b: u64,
}

impl Fibonacci {
    fn new() -> Self {
        Fibonacci { a: 0, b: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let current = self.a;
        self.a = self.b;
        self.b = self.b.checked_add(current)?;
        Some(current)
    }
}

fn main() {
    let fib = Fibonacci::new();
    for (i, val) in fib.take(10).enumerate() {
        println!("Fib {}: {}", i, val);
    }
}

Key takeaway: Custom iterators let you express infinite sequences with zero heap allocation. checked_add prevents overflow, a safety pattern Rust enforces by default.

8. WebAssembly Interop with JavaScript Callbacks

Scenario: You want your Rust WASM module to call a JavaScript function (e.g., console.log).

Prompt:

"Write a Rust function run_js_callback that accepts a js_sys::Function argument and calls it with a string \"Hello from Rust!\". Use wasm-bindgen and js_sys. Then show the JavaScript code that passes console.log as the callback."

Example Result:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn run_js_callback(callback: &js_sys::Function) {
    let msg = JsValue::from_str("Hello from Rust!");
    let this = JsValue::null();
    let _ = callback.call1(&this, &msg);
}
import init, { run_js_callback } from './pkg/my_wasm.js';

await init();
run_js_callback(console.log);
// Console: "Hello from Rust!"

Key takeaway: js_sys::Function allows two-way interop. The prompt teaches you to pass JavaScript functions into Rust WASM and invoke them safely.

9. Memory-Safe FFI with libc and std::ffi

Scenario: You need to call a C function int add(int a, int b) from Rust using FFI.

Prompt:

"Write a Rust program that links to an external C library libadd containing int add(int a, int b). Use extern \"C\" and std::ffi::c_int. Handle linking via a build script (build.rs). Print the result of add(3, 4)."

Example Result:

// build.rs
fn main() {
    println!("cargo:rustc-link-lib=add");
}
// main.rs
use std::ffi::c_int;

extern "C" {
    fn add(a: c_int, b: c_int) -> c_int;
}

fn main() {
    unsafe {
        let result = add(3, 4);
        println!("3 + 4 = {}", result);
    }
}

Key takeaway: FFI calls are inherently unsafe—Rust cannot verify C code’s memory safety. The prompt forces you to contain the unsafe block and use the correct integer types (c_int).

10. Embedded Rust: Blinking an LED on Raspberry Pi Pico

Scenario: You want to blink an LED on a Raspberry Pi Pico (RP2040) using Rust without the Arduino ecosystem.

Prompt:

"Write a Rust program for the Raspberry Pi Pico (RP2040) using rp2040-hal and cortex-m-rt. Configure GPIO pin 25 (built-in LED) as output, then blink it with a 500ms delay in an infinite loop. Use embedded_hal::digital::OutputPin trait."

Example Result:

#![no_std]
#![no_main]

use cortex_m_rt::entry;
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;
use panic_halt as _;
use rp2040_hal::{clocks::Clock, pac, watchdog::Watchdog, Sio};

#[entry]
fn main() -> ! {
    let mut pac = pac::Peripherals::take().unwrap();
    let core = pac::CorePeripherals::take().unwrap();
    let mut watchdog = Watchdog::new(pac.WATCHDOG);
    let sio = Sio::new(pac.SIO);

    let clocks = rp2040_hal::clocks::init_clocks_and_plls(
        rp2040_hal::fugit::RateExtU32::Hz(12_000_000),
        &mut pac.XOSC,
        &mut pac.CLOCKS,
        &mut pac.PLL_SYS,
        &mut pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let pins = rp2040_hal::gpio::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    let mut led_pin = pins.gpio25.into_push_pull_output();
    let mut delay = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);

    loop {
        led_pin.set_high().unwrap();
        delay.delay_ms(500);
        led_pin.set_low().unwrap();
        delay.delay_ms(500);
    }
}

Key takeaway: Embedded Rust uses the same ownership and trait system as desktop Rust. The prompt introduces #[entry], no_std, and hardware abstraction layers (HAL).

11. Asynchronous HTTP Client with reqwest

Scenario: You need to fetch data from a REST API concurrently without blocking threads.

Prompt:

"Write an async Rust function that uses reqwest to fetch two URLs concurrently using tokio::join!. Print the status codes and first 100 characters of each response body. Use tokio runtime."

Example Result:

use reqwest;
use tokio;

async fn fetch_url(url: &str) -> Result<(String, String), reqwest::Error> {
    let resp = reqwest::get(url).await?;
    let status = resp.status().to_string();
    let body = resp.text().await?;
    let snippet = body.chars().take(100).collect();
    Ok((status, snippet))
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let url1 = "https://httpbin.org/get";
    let url2 = "https://example.com";

    let ((s1, b1), (s2, b2)) = tokio::join!(
        fetch_url(url1),
        fetch_url(url2)
    );

    println!("URL1: status={}, body={}", s1, b1);
    println!("URL2: status={}, body={}", s2, b2);
    Ok(())
}

Key takeaway: Async Rust with tokio is zero-cost; you don’t pay for concurrency you don’t use. join! runs futures concurrently, not in sequence.

12. File Watching with notify Crate

Scenario: You want a CLI tool that watches a directory and prints notifications when files are created, modified, or deleted.

Prompt:

"Write a Rust program using the notify crate that watches a given directory (passed as CLI argument). On any event, print the event type and path. Use RecursiveMode::Recursive to watch subdirectories. Handle errors gracefully."

Example Result:

use notify::{Config, Event, RecursiveMode, Watcher};
use std::path::Path;

fn main() -> notify::Result<()> {
    let args: Vec<String> = std::env::args().collect();
    let path = if args.len() > 1 {
        args[1].clone()
    } else {
        ".".to_string()
    };

let mut watcher = notify::recommended_watcher(

|res: Result<Event, notify::Error>| {
        match res {
            Ok(event) => println!("Event: {:?} on {:?}", event.kind, event.paths),
            Err(e) => eprintln!("Watch error: {}", e),
        }
    })?;

    watcher.watch(Path::new(&path), RecursiveMode::Recursive)?;

    println!("Watching {}. Press Ctrl+C to stop.", path);
    loop {
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
}

Key takeaway: The notify crate abstracts cross-platform file system events. The prompt teaches event-driven programming with a callback closure.

Conclusion

These 12 prompts cover the most common Rust development scenarios: safe concurrency, CLI tools, WebAssembly interop, error handling, serialization, networking, custom iterators, FFI, embedded systems, async HTTP, and file watching. Each prompt is designed to be a building block you can adapt to your own projects.

Rust's strength lies in its ability to guarantee memory safety without a garbage collector. By mastering these patterns, you’ll write code that is not only correct but also performant and maintainable. Try adapting one of these prompts to your current project today.

← All posts

Comments