A self-contained, fullstack tutorial app for learning Rust end-to-end, built for engineers who already know Node/TypeScript and need to be interview-ready for a senior Rust role.
The curriculum walks the language from scratch, then layers on ownership, lifetimes, traits, async/await, memory & performance, senior pitfalls, and the algorithms that show up in coding interviews. Each lesson has a TS comparison where helpful, an editable example, and compiles/runs your code for real, not simulated.
The backend is written in idiomatic Rust using axum + tokio + reqwest plus a handful of small, well-known crates — read its source as a second tutorial. The frontend is plain HTML / CSS / vanilla JS, no build step.
┌──────────────────────────────────────────────────────────────────┐
│ Sidebar lessons │ Lesson description + key takeaways │
│ Search / progress bar │ │
│ │ ┌────────────────────────────────┐ │
│ ✓ Welcome & Setup │ │ Editable Rust code │ │
│ ○ Variables & Mutab… │ │ (real rustc on backend) │ │
│ ○ Ownership Rules │ └────────────────────────────────┘ │
│ ○ ... │ ┌────────────────────────────────┐ │
│ │ │ Output / stderr / errors │ │
│ │ └────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
You need Rust 1.75+ installed (rustup default stable is fine). The curriculum uses async/await (stable since 1.39),
let … else (1.65), GATs (1.65), and a few 2021-edition idioms.
Latest stable is recommended.
cargo run
# Rust tutorial running at http://localhost:8080Open http://localhost:8080 in your browser.
cargo run -- -addr :9000 # bind to a different portEach lesson has a floating Ask AI button that opens a chat panel. The question is sent to an LLM along with the current lesson's title, description, code example, key takeaways, AND whatever you've typed in the editor, so you can ask things like "why won't this compile?" and get a specific answer.
The server auto-detects which provider to use from your environment, in this priority order: Google → Anthropic → OpenAI.
Set things up via a .env file. Copy the example and fill in one key:
cp .env.example .env
# edit .env and paste your key
cargo runA .env file in the project root is loaded automatically on startup.
Get a free Gemini key at https://aistudio.google.com/apikey.
Real environment variables always win over the .env file, so you can
also do either of these without touching the file:
# One-shot for a single run:
GEMINI_API_KEY=AIza… cargo run
# Or shell-export:
export GEMINI_API_KEY=AIza…
cargo runOther supported variables:
| Variable | Default | Notes |
|---|---|---|
GEMINI_API_KEY |
none | Google Gemini (preferred) |
GEMINI_MODEL |
gemini-2.5-flash |
e.g. gemini-2.5-pro |
ANTHROPIC_API_KEY |
none | Anthropic Claude |
ANTHROPIC_MODEL |
claude-haiku-4-5 |
|
OPENAI_API_KEY |
none | OpenAI |
OPENAI_MODEL |
gpt-4o-mini |
When AI chat is enabled on a public deployment, these limits apply per client IP
(Cloud Run sets X-Forwarded-For automatically; set TRUST_PROXY=1 behind other
reverse proxies):
| Variable | Default | Notes |
|---|---|---|
CHAT_RATE_PER_MIN |
5 |
Burst limit; set 0 to disable |
CHAT_RATE_DAILY |
50 |
Daily quota per IP; set 0 to disable |
CHAT_MAX_MESSAGE_CHARS |
4000 |
Max characters per message |
CHAT_MAX_CODE_CHARS |
16000 |
Max editor code sent with each turn |
CHAT_MAX_BODY_BYTES |
65536 |
Max JSON body size for /api/chat |
Exceeded limits return HTTP 429 with a Retry-After header. Limits are also
exposed via GET /api/chat/status under limits.
On startup the server logs which provider is active:
AI chat enabled via google (gemini-2.5-flash)
Rust tutorial listening on http://0.0.0.0:8080
If no key is set, the chat panel still opens but explains how to configure one. Everything else in the app still works without an API key.
Chat history is kept per lesson, in localStorage: switch lessons
and you get a fresh thread; the trash icon clears the current
thread. Press Esc or click the × to close the panel.
The curriculum is "zero → interview-ready for a senior Rust role." It assumes you already write Node/TypeScript at a senior level. Each lesson is editable and compiles/runs for real on your machine.
The lessons are grouped into 16 sections. The first 11 sections teach the language end-to-end; the last 5 are the senior / interview track.
| Section | Lessons |
|---|---|
| Basics | Welcome & Setup · Coming from TypeScript: Mental Model · Variables & Mutability · Basic Types & Conversions · Constants and Static |
| Control Flow | if, loop, while, for · match and Patterns · Functions & Return Values · Closures and Iterators · Option and the ? Operator · Early Returns & Guard Clauses |
| Ownership & Borrowing | Ownership Rules · References & Borrowing · Move vs Copy vs Clone · Slices · String vs &str · Box / Rc / Arc |
| Data Structures | Vec · Arrays & Slices · HashMap & BTreeMap · Structs · Enums (ADTs) · Tuples & Destructuring |
| Traits & Generics | Methods via impl · Traits · Generic Functions & Bounds · Trait Objects (dyn) · Derivable Traits · The Iterator Trait · Monomorphization |
| Errors | Result<T,E> & ? · panic! vs Result · Custom Error Types · Error Handling Patterns |
| Concurrency | Threads · Channels (mpsc) · Arc<Mutex> · Send / Sync · async / await: The Idea · Thread Safety Patterns |
| Tooling & Packages | Cargo · rustfmt & clippy · Modules & Visibility · Writing Tests · Benchmarks & Profiling |
| Standard Library | Files & io::Read/Write · Strings & Formatting · Collections Deep Dive · time: Duration & Instant · Logging with log/tracing |
| Web Development | HTTP Server (axum) · HTTP Client (reqwest) · Middleware with Tower · Graceful Shutdown |
| Ecosystem | Web Frameworks: axum, actix, Rocket, warp · What Rust Is Great At |
| Section | Lessons |
|---|---|
| Lifetimes | Lifetime Annotations · Elision Rules · 'static & HRTB · PhantomData & Variance |
| Memory & Performance | Stack vs Heap · Zero-Cost Iterators · Reducing Allocations |
| Senior Pitfalls | Borrow-Checker Battles · Interior Mutability · Unsafe, Send/Sync, FFI |
| Interview Algorithms | Two Pointers · Sliding Window · Binary Search & Variants · Backtracking · Dynamic Programming · Graph Traversal (BFS/DFS) · Heaps & Top-K · Linked Lists in Rust · LRU Cache |
| Interview Prep | Senior Rust Interview Cheatsheet |
Progress is tracked in localStorage. Use the "Reset progress" button
in the sidebar to clear it.
The codebase is organized by domain, not by technical layer. Each
module owns its types AND its access layer; the api module sits on
top and depends on all the domains, but no domain depends on HTTP.
main.rs is just the composition root.
404skills-rust/
├── Cargo.toml # crate manifest + dependencies
├── Cargo.lock # resolver output (committed for binaries)
├── src/
│ ├── main.rs # composition root: load config, wire deps, start server
│ │
│ ├── config/ # .env loader
│ │ ├── mod.rs
│ │ └── dotenv.rs
│ │
│ ├── lesson/ # lesson domain. Lesson type + Repository port + catalog.
│ │ ├── mod.rs # Lesson, Summary, Repository, InMemoryRepository
│ │ ├── catalog.rs # composes per-category slices into the ordered Catalog
│ │ ├── lessons_basics.rs # Foundations: Basics → Ecosystem (one file per category)
│ │ ├── lessons_control_flow.rs
│ │ ├── lessons_ownership.rs
│ │ ├── lessons_data_structures.rs
│ │ ├── lessons_traits_generics.rs
│ │ ├── lessons_errors.rs
│ │ ├── lessons_concurrency.rs
│ │ ├── lessons_tooling.rs
│ │ ├── lessons_stdlib.rs
│ │ ├── lessons_web.rs
│ │ ├── lessons_ecosystem.rs
│ │ ├── lessons_lifetimes.rs # Senior track: Lifetimes → Interview Prep
│ │ ├── lessons_memory.rs
│ │ ├── lessons_pitfalls.rs
│ │ ├── lessons_algorithms.rs
│ │ └── lessons_interview_prep.rs
│ │
│ ├── runner/ # pluggable code sandbox
│ │ ├── mod.rs # Runner trait + from_env selector
│ │ ├── local.rs # `rustc` in a temp dir (dev default)
│ │ └── playground.rs # POST to play.rust-lang.org (cloud default)
│ │
│ ├── tutor/ # LLM chat domain
│ │ ├── mod.rs
│ │ ├── service.rs # Service + system-prompt builder + stripHTML
│ │ ├── provider.rs # Provider trait + select_from_env
│ │ ├── httpx.rs # shared post_json helper (DRY across providers)
│ │ ├── gemini.rs # Google Gemini adapter
│ │ ├── anthropic.rs # Anthropic Claude adapter
│ │ └── openai.rs # OpenAI adapter
│ │
│ └── api/ # HTTP transport, no business logic here
│ ├── mod.rs # Router, log middleware, AppState
│ ├── lessons.rs # GET /api/lessons{,/:id}
│ ├── run.rs # POST /api/run
│ ├── chat.rs # POST /api/chat, GET /api/chat/status
│ └── static_assets.rs # serves the embedded web/ via rust-embed
│
├── cmd/smoke/main.rs # smoke-tests the curriculum via /api/run
├── Dockerfile # multi-stage build, distroless runtime (~20 MB)
├── .dockerignore
│
└── web/ # frontend (embedded into the binary)
├── index.html
├── styles/
│ ├── tokens.css # design tokens (light + dark theme)
│ ├── base.css # resets, page grid
│ ├── components.css # shared buttons + icon-btn
│ ├── sidebar.css
│ ├── lesson.css
│ ├── playground.css
│ └── chat.css
└── js/ # ES modules, no build step
├── app.js # composition root: imports + init order
├── state.js # shared state container + localStorage
├── dom.js # $ / $$ query helpers
├── api.js # all fetch() calls live here
├── theme.js
├── lessons.js # sidebar + nav + render + progress
├── playground.js # editor + run + output
├── chat.js # chat panel + per-lesson history
└── markdown.js # safe md subset for chat replies
Highlights worth opening in your editor:
src/main.rs: pure wiring. tracing init, dependency construction, axum server with graceful shutdown.src/lesson/mod.rs: theRepositorytrait pattern. Today it'sInMemoryRepository; tomorrow you could add aFileRepositorywithout changing the api or tutor modules.src/lesson/catalog.rs: single composition point for lesson order. Eachlessons_<category>.rsexposes alessons()function returning aVec<Lesson>; the catalog stitches them in pedagogical order.src/tutor/provider.rs+httpx.rs: theProvidertrait and the onepost_jsonhelper that all three LLM adapters share. Compare the three provider files to see how the protocol-specific differences are isolated.src/runner/: pluggable sandbox.mod.rsdefines theRunnertrait;local.rsshells out torustc(dev default);playground.rsPOSTs to the Rust Playground API (cloud default). Pick at startup withRUNNER=local|playgroundor rely on auto-detect.src/api/static_assets.rs: usesrust-embedto bake the entireweb/directory into the binary at compile time.web/js/app.js: the frontend composition root mirrors the backend.
cargo build --release
./target/release/rust-tutCross-compile for another platform:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslThe web assets are baked into the binary via rust-embed.
The /api/run endpoint executes user-submitted Rust code through a
pluggable backend. Pick one with the RUNNER environment variable:
RUNNER |
Backend | Use for | Safety |
|---|---|---|---|
local |
rustc in a temp dir |
Local dev | UNSAFE in public |
playground |
POST to https://play.rust-lang.org/execute | Cloud deploys | Safe — community sandbox |
| unset | Auto: playground if $K_SERVICE is set (Cloud Run / Knative), else local |
Anywhere | Picks the safe default |
Local backend. Convenient on a laptop, but it runs untrusted code
with the same UID, filesystem, and network access as the server
process. It's only suitable for cargo run on your own machine. Do
not expose a Local-backend instance to the public internet.
Playground backend. Sends the source to the community-run Rust
Playground service (the same one that powers
https://play.rust-lang.org). User code runs inside a hardened
sandbox with no network, restricted filesystem, and a hard wall-clock
limit. This is the only safe choice for a public-facing deployment.
Override the endpoint with PLAYGROUND_URL if you self-host the
playground (the source is at
rust-lang/rust-playground).
A multi-stage Dockerfile produces a small image that defaults to the
Playground runner, listens on $PORT, exposes /healthz for probes,
and shuts down gracefully on SIGTERM.
One-command deploy (uses Cloud Build under the hood, no local Docker needed):
gcloud run deploy rust-tut \
--source . \
--region us-central1 \
--allow-unauthenticatedIf you want LLM-powered "Ask AI", pass the key as a secret-backed env var:
# Store the key in Secret Manager:
echo -n "AIza..." | gcloud secrets create gemini-key --data-file=-
# Reference it on deploy:
gcloud run deploy rust-tut \
--source . \
--region us-central1 \
--allow-unauthenticated \
--update-secrets=GEMINI_API_KEY=gemini-key:latestThe default RUNNER=playground is set in the Dockerfile, and the
in-app auto-detection (via $K_SERVICE) would pick Playground anyway
if it weren't.
docker build -t rust-tut .
docker run --rm -p 8080:8080 rust-tut
# open http://localhost:8080The image runs as non-root, ignores the bundled .env (use
-e GEMINI_API_KEY=... instead), and defaults to the Playground
runner.
After you finish the lessons, try these projects to cement the language:
- JSON CRUD service: axum + sqlx + Postgres. Add tests with
tower::ServiceExt. - CLI tool: clap + anyhow + serde. Cross-compile to Linux / macOS / Windows.
- Async pipeline: tokio + channels, with
tokio::select!cancellation and graceful shutdown. - WASM module: compile a small Rust library to WebAssembly with
wasm-bindgenand call it from JS.
Recommended reading:
- The Rust Book
- Rust by Example
- The Rustonomicon (unsafe Rust)
- Asynchronous Programming in Rust
- Jon Gjengset's "Crust of Rust" videos.
MIT © 2026 0xm1kr