Back to writing

AI won't teach you Rust (but it can help you teach yourself)

How I'm using Claude Code as a mentor, not an autopilot, to learn Rust through a real project, and why going slow is the whole point.

rust claude-code ai-engineering learning

I’m a senior TypeScript engineer. In my day-to-day work as a tech lead, I barely write code anymore. When I do, it’s mostly orchestrating AI tools: reviewing generated code, refining prompts, nudging outputs in the right direction. The code gets shipped, but the craft of writing it has quietly slipped away from my routine.

So when I decided to learn Rust, the obvious move in 2026 would be to prompt my way through it. Ask Claude to generate a project, copy the output, fix the compiler errors with more prompts, and call it done. I’d have working Rust code in an afternoon. I’d also have learned absolutely nothing.

This article is about the approach I’m using instead: treating AI as a mentor rather than an autopilot, building a real project from scratch, and deliberately going slow. It’s about the rules I set for myself, why they matter, and what the research says about why most people skip them.

Why Rust, why now

Rust has been on my radar for a while. Not because of hype, but because the language introduces concepts I’ve never had to think about in TypeScript. Ownership and borrowing. Result and Option as first-class error handling. A type system that’s genuinely strict, not “strict if you squint and ignore the any types.” A compiler that refuses to let you ship certain categories of bugs.

These aren’t just Rust features. They’re mental models that make you a better engineer in any language. Understanding why Rust forces you to think about memory ownership changes how you reason about state in TypeScript too. Result<T, E> as a pattern teaches you something about error handling that try/catch never will.

I want to learn Rust properly for two reasons. First, to grow as a software engineer. Learning a language with fundamentally different constraints forces you to rethink assumptions you didn’t even know you had. Second, because I’d genuinely love to work with Rust professionally. But “I once prompted an AI to write me a web server in Rust” isn’t going to get me there. I need to actually understand the language.

The “full AI” trap

There’s a growing body of research showing that passive AI use actively harms learning. A randomized controlled trial by Barcaui et al. tested 120 participants over 45 days and found that AI-assisted learners scored 57.5% on retention tests, compared to 68.5% for those who learned traditionally. That’s an 11-point gap, with a medium-to-large effect size. The mechanism is straightforward: when the AI does the thinking, your brain doesn’t encode the knowledge deeply. Cognitive offloading is the technical term. Copy-pasting is the practical one.

It gets worse. A study by METR on experienced open-source developers found that those using AI tools believed they were 20% more productive but were actually 19% slower. That’s a nearly 40-point perception gap. You feel like you’re flying, but you’re not. Applied to learning, this means you can spend an evening “learning Rust with AI” and come away convinced you understood ownership, when what you actually understood was how to approve Claude’s suggestions.

Simon Willison made a useful distinction between vibe coding (you don’t read the code, you just see if it works) and genuine AI-assisted development (the AI wrote it, but you’ve reviewed, tested, and understood every line). For learning, there’s a third mode that matters even more: the AI explains, and you write. That’s where knowledge is built.

The problem is that most people default to the first mode and convince themselves they’re operating in the third.

A real project, not a tutorial

I don’t learn well from tutorials. I learn by building something real, hitting walls, and figuring my way through. So instead of “Build a CLI tool in Rust,” I picked a project with actual constraints: a probative-value archiving system.

The short version: in French law, businesses can archive invoices and receipts digitally instead of keeping paper, but only if the archiving system proves integrity, authenticity, traceability, durability, and reversibility. There’s a specific legal framework (NF Z42-013, EU eIDAS regulation, French tax code). Shortcuts are impossible. If the system doesn’t meet the requirements, the archived documents have no legal value.

This matters for learning because the constraints are real. I can’t skip error handling because “it’s just a learning project.” I can’t ignore edge cases because legal probative force depends on getting them right. The domain forces rigor in a way that a todo app never will.

The project is structured in iterations. Iteration 0 is a proof of concept: basic HTTP endpoints, file upload, SHA-256 hashing. Later iterations add hash-chained journals, RFC 3161 timestamps, multi-tenancy, PDF normalization, and eventually a standalone offline verifier. Each iteration introduces new Rust concepts naturally. I’m not learning generics because a tutorial told me to. I’m learning them because my config system needs a testable environment variable provider.

The protocol I settled on

Before starting, I wrote explicit instructions in the project’s CLAUDE.md file. This is the configuration file that shapes how Claude Code behaves in a project. Mine includes a section called “Pedagogical priorities” that lists Rust concepts in learning order, and a set of rules:

  • Guide first, code second. Explain why before what.
  • Frame Rust in TypeScript terms when it helps. Traits are like interfaces with default implementations and dispatch. Result<T, E> is like a typed Either.
  • Do NOT write code unless explicitly asked.

That last rule is the most important one. By default, AI wants to give you the answer. It’s trained to be helpful, and “helpful” usually means “here’s the code.” For learning, that’s the worst possible behavior.

In practice, my workflow looks like this:

Step 1: Research first, AI second. When I encounter a new concept, I start with the Rust Book, Stack Overflow, Reddit, and the standard library docs. Old-school research. I read explanations from multiple perspectives, look at examples, and try to form a mental model before involving AI.

Step 2: Try, fail, try again. I write the code myself. It usually doesn’t compile. Rust’s compiler is famously helpful, but “famously helpful” assumes you understand the concepts behind the errors. When you’re new, an error about lifetimes or borrowed values can be opaque. I still try to understand the compiler’s message first, but I’m honest about when I’m stuck.

Step 3: Ask for guidance, not answers. When I’m against a wall, I ask Claude for direction. Not “write me the function,” but “I’m trying to do X and getting this error, what concept am I missing?” The key is never asking for the full answer. A nudge, a concept explanation, a pointer to the right documentation section.

Step 4: Post-implementation review. After I write working code, then I ask Claude to review it. Like a mentor looking over a junior developer’s pull request. What’s idiomatic? What’s not? What Rust patterns am I missing? This is where I’ve written about using AI as a reviewer in a professional context, and the same principle applies to learning: review after implementation, not generation before it.

The protocol in action

Here’s a concrete example. Early in the project, I needed a configuration system that reads environment variables. In TypeScript, this is trivial: process.env.PORT. In Rust, it’s a chance to learn three things at once: error handling, generics, and closures.

I started by writing a simple Config struct with from_env(). It worked, but it was untestable because it called std::env::var directly. In TypeScript, I’d inject a dependency. I knew the pattern, but not the Rust syntax.

So I researched. How do closures work in Rust? What’s the Fn trait? Why does the compiler care about whether a closure borrows or moves? I read, tried things, got compiler errors, and tried again.

When I got stuck on the where clause syntax, I asked Claude: “I want to pass a function that acts like env::var but can be swapped in tests. What’s the Rust pattern for this?” Claude explained the Fn trait bound approach. I wrote the implementation myself:

pub fn from_provider<F>(get_var: F) -> Result<Self, ConfigError>
where
    F: Fn(&str) -> Result<String, std::env::VarError>,
{
    let host = get_var("HOST")
        .map_err(|_| ConfigError::MissingVar { name: "HOST" })?;
    let port_str = get_var("PORT")
        .map_err(|_| ConfigError::MissingVar { name: "PORT" })?;
    let port = port_str
        .parse::<u16>()
        .map_err(|_| ConfigError::InvalidPort {
            value: port_str,
        })?;
    // ...
    Ok(Config { host, port, storage_root })
}

Then I asked Claude to review it. The feedback: the pattern is idiomatic, the error mapping is clean, and the generic approach is exactly how the Rust ecosystem handles testable I/O boundaries. I also learned about the ? operator’s error propagation chain, map_err for converting between error types, and why Rust’s parse returns a Result instead of throwing.

One function. Three concepts learned. And because I wrote it myself, after struggling with the syntax, those concepts stuck.

Compare this with what would have happened if I’d asked Claude to “write me a testable config system in Rust.” I’d have the same code in 10 seconds. I’d understand none of it.

TDD as a learning accelerator

I’m doing test-driven development on this project, which might seem like overkill for a learning exercise. It’s not. TDD forces you to think about what the code should do before you think about how to write it. In a language you’re still learning, that separation is invaluable.

Writing a test first means I have to understand the function’s contract: what goes in, what comes out, what should fail. Only then do I tackle the Rust syntax to make it pass. It slows things down considerably, but that’s precisely the point.

Here’s what a test-first cycle looks like in practice. I write a test like “archiving a valid file returns a 200 with a UUID and a SHA-256 hash.” This forces me to learn how to construct a multipart HTTP request in Rust, how to parse a JSON response, and how async test functions work with Tokio. Each test is a micro-learning exercise disguised as a quality practice.

The builder pattern emerged naturally from testing needs. I needed to construct multipart form data for integration tests, so I built a MultipartBuilder with a fluent API: MultipartBuilder::valid() for the happy path, .without_file() for edge cases. Writing that builder taught me more about ownership, Vec<u8> byte manipulation, and String vs &str than any tutorial would have.

#[tokio::test]
async fn returns_400_when_no_file() {
    let state = test_state();
    let body = MultipartBuilder::valid()
        .without_file()
        .build();

    let response = router(state)
        .oneshot(/* request with body */)
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

Red. Green. Refactor. Each cycle takes longer than it would in TypeScript, but each cycle teaches me something I’ll remember.

Why slow is the point

The project moves slowly. I could ship the same functionality in an afternoon with TypeScript. In Rust, with this learning protocol, iteration 0 took weeks. And that’s exactly right.

There’s a concept in learning science called “desirable difficulty.” The idea is that struggle during learning, the kind that forces you to think hard, retrieve knowledge, and make connections, is not an obstacle to learning. It is the learning mechanism. When things feel easy, you’re often not encoding deeply. When they feel hard, that’s your brain doing the work that creates lasting knowledge.

For me, there’s also something deeply refreshing about it. In my daily work, I’m mostly orchestrating: reviewing AI-generated code, making architectural decisions, unblocking people. The act of sitting down and writing code myself, getting stuck, fighting the compiler, eventually making a test pass, feels like reconnecting with why I became an engineer in the first place.

I wrote about different levels of AI interaction before: oracle mode (one prompt, one answer), conversation mode (back and forth), and structured facilitation (opinionated phases with gates). My Rust learning protocol is closest to structured facilitation: the AI has a defined role (mentor, not author), there are explicit rules about what it can and can’t do, and the human stays in the driver’s seat. The structure is what prevents it from collapsing into vibe coding.

Knowing what you don’t know

Here’s the honest part. I can write basic Rust now. I can set up an Axum server, handle multipart uploads, hash files with SHA-256, write integration tests, and implement custom error types with thiserror. That’s real progress.

But I still don’t deeply understand ownership and borrowing. I’ve encountered them in my code. I’ve dealt with compiler errors about moved values and borrowed references. I’ve read explanations. But I haven’t yet hit the moment where I need to truly master them to solve a problem. That moment is coming, as the project grows in complexity, and I know it.

This honest self-assessment is, I think, the most important skill in AI-assisted learning. The danger isn’t using AI. The danger is losing the ability to evaluate what you actually know versus what the AI knows for you. When Claude fixes a borrowing error, did I learn something, or did I just learn to ask Claude? The answer depends entirely on whether I can explain the fix without Claude’s help.

My rule is simple: if I can’t explain it, I don’t understand it, and I don’t move on. Sometimes that means spending 30 minutes on a concept that Claude could resolve in 3 seconds. That’s not inefficiency. That’s the whole point.

Steve Simkins wrote about a similar approach: strict rules where the AI explains and the human writes every line. Vincent Bruijn documented the opposite experience: letting Claude write most of the code and learning enough Rust in one evening to build an HTTP server. Both are valid, but they optimize for different things. Bruijn optimized for speed and got a working project. Simkins and I optimize for retention and get slower projects that actually teach us Rust.

The question you have to answer for yourself is: do you want to have built something in Rust, or do you want to know Rust? The AI makes both possible. Only one of them sticks.

TL;DR

  • AI makes it trivially easy to produce working Rust code. That’s not the same as learning Rust. The research shows passive AI use creates an 11-point retention gap and a dangerous perception of competence.
  • Set explicit rules in your project’s CLAUDE.md: AI explains, you write. Research first, AI second. Never accept the full answer.
  • Go slow on purpose. The struggle is the learning mechanism, not an obstacle to it. “Desirable difficulty” is a well-documented phenomenon in learning science.
  • Use AI as a post-implementation reviewer, not a pre-implementation author. The mentor pattern (write first, review after) builds knowledge that the oracle pattern (ask first, copy after) destroys.
  • Track your weak spots honestly. If you can’t explain a concept without AI help, you don’t understand it yet. That self-awareness is more valuable than any amount of generated code.