Jump to content

Build Your First WebAssembly App in Rust in 10 Minutes

From JOHNWICK
Revision as of 15:59, 21 November 2025 by PC (talk | contribs) (Created page with "Hey there! I’ve been exploring WebAssembly with Rust lately, and I thought I’d share my recent learning with you. Instead of the usual React + Web API combo we’re all used to, I decided to see what it takes to build an entire web app in Rust today. Spoiler alert: it’s surprisingly straightforward! If you’re interested in more findings like this, let me know and I can share more in my next blog post. For now, let’s dive into building a practical password stre...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Hey there! I’ve been exploring WebAssembly with Rust lately, and I thought I’d share my recent learning with you. Instead of the usual React + Web API combo we’re all used to, I decided to see what it takes to build an entire web app in Rust today. Spoiler alert: it’s surprisingly straightforward!

If you’re interested in more findings like this, let me know and I can share more in my next blog post. For now, let’s dive into building a practical password strength checker that runs entirely in your browser using Rust and WebAssembly.

What We’re Building

We’ll create a real-time password strength checker that analyse passwords as you type. No server calls, no external APIs — just pure Rust running in the browser through WebAssembly. Pretty cool, right?

Getting Started

First things first, we assume that you have rust Installed if not then you can follow up the rust website tutorial to install rust.

Install WebAssembly Package

Let’s use the official WebAssembly template to get started quickly:

cargo generate --git https://github.com/rustwasm/wasm-pack-template

When prompted, name your project password-checker, then navigate into it: if above command doesn’t work make sure to install the generator via cargo.

cd password-checker

The Rust Code Now for the fun part! Replace the contents of src/lib.rs with our password checker logic:

mod utils;
use wasm_bindgen::prelude::*;

// Import the `console.log` function from the browser
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}
// Define a macro for easier console logging
macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
// Struct to represent password analysis results
#[wasm_bindgen]
pub struct PasswordAnalysis {
    score: u8,
    feedback: String,
    has_lowercase: bool,
    has_uppercase: bool,
    has_numbers: bool,
    has_symbols: bool,
    length_ok: bool,
}
#[wasm_bindgen]
impl PasswordAnalysis {
    #[wasm_bindgen(getter)]
    pub fn score(&self) -> u8 {
        self.score
    }
    #[wasm_bindgen(getter)]
    pub fn feedback(&self) -> String {
        self.feedback.clone()
    }
    #[wasm_bindgen(getter)]
    pub fn has_lowercase(&self) -> bool {
        self.has_lowercase
    }
    #[wasm_bindgen(getter)]
    pub fn has_uppercase(&self) -> bool {
        self.has_uppercase
    }
    #[wasm_bindgen(getter)]
    pub fn has_numbers(&self) -> bool {
        self.has_numbers
    }
    #[wasm_bindgen(getter)]
    pub fn has_symbols(&self) -> bool {
        self.has_symbols
    }
    #[wasm_bindgen(getter)]
    pub fn length_ok(&self) -> bool {
        self.length_ok
    }
}
// Main function to analyze password strength
#[wasm_bindgen]
pub fn analyze_password(password: &str) -> PasswordAnalysis {
    utils::set_panic_hook();
    console_log!("Analyzing password of length: {}", password.len());
    let has_lowercase = password.chars().any(|c| c.is_lowercase());
    let has_uppercase = password.chars().any(|c| c.is_uppercase());
    let has_numbers = password.chars().any(|c| c.is_numeric());
    let has_symbols = password.chars().any(|c| !c.is_alphanumeric());
    let length_ok = password.len() >= 8;
    let mut score = 0u8;
    let mut feedback_parts = Vec::new();
    // Check length
    if length_ok {
        score += 20;
    } else {
        feedback_parts.push("Use at least 8 characters".to_string());
    }
    // Check character types
    if has_lowercase {
        score += 15;
    } else {
        feedback_parts.push("Add lowercase letters".to_string());
    }
    if has_uppercase {
        score += 15;
    } else {
        feedback_parts.push("Add uppercase letters".to_string());
    }
    if has_numbers {
        score += 15;
    } else {
        feedback_parts.push("Add numbers".to_string());
    }
    if has_symbols {
        score += 15;
    } else {
        feedback_parts.push("Add symbols (!@#$%^&*)".to_string());
    }
    // Bonus points for longer passwords
    if password.len() >= 12 {
        score += 10;
    }
    // Check for common patterns
    if !has_common_patterns(password) {
        score += 10;
    } else {
        feedback_parts.push("Avoid common patterns".to_string());
        score = score.saturating_sub(10);
    }
    let feedback = if feedback_parts.is_empty() {
        "Strong password!".to_string()
    } else {
        feedback_parts.join(", ")
    };
    PasswordAnalysis {
        score,
        feedback,
        has_lowercase,
        has_uppercase,
        has_numbers,
        has_symbols,
        length_ok,
    }
}
// Helper function to check for common patterns
fn has_common_patterns(password: &str) -> bool {
    let common_patterns = vec![
        "123456", "password", "qwerty", "abc123", "admin", "letmein",
        "welcome", "monkey", "dragon", "master"
    ];
    let lower_password = password.to_lowercase();
    
    for pattern in common_patterns {
        if lower_password.contains(pattern) {
            return true;
        }
    }
    // Check for repeated characters
    if password.len() >= 3 {
        let chars: Vec<char> = password.chars().collect();
        for window in chars.windows(3) {
            if window[0] == window[1] && window[1] == window[2] {
                return true;
            }
        }
    }
    false
}
// Function to generate a random password suggestion
#[wasm_bindgen]
pub fn generate_password_suggestion() -> String {
    utils::set_panic_hook();
    let lowercase = "abcdefghijklmnopqrstuvwxyz";
    let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    let numbers = "0123456789";
    let symbols = "!@#$%^&*";
    let mut password = String::new();
    
    // Ensure we have at least one of each type
    password.push(get_random_char(lowercase));
    password.push(get_random_char(uppercase));
    password.push(get_random_char(numbers));
    password.push(get_random_char(symbols));
    // Fill the rest randomly
    let all_chars = format!("{}{}{}{}", lowercase, uppercase, numbers, symbols);
    for _ in 4..12 {
        password.push(get_random_char(&all_chars));
    }
    // Simple shuffle
    let mut chars: Vec<char> = password.chars().collect();
    for i in 0..chars.len() {
        let j = (js_sys::Math::random() * chars.len() as f64) as usize % chars.len();
        chars.swap(i, j);
    }
    chars.into_iter().collect()
}
// Helper function to get a random character from a string
fn get_random_char(chars: &str) -> char {
    let chars_vec: Vec<char> = chars.chars().collect();
    let index = (js_sys::Math::random() * chars_vec.len() as f64) as usize % chars_vec.len();
    chars_vec[index]
}

Update your Cargo.toml file to include the necessary dependencies:

[package]
name = "password-checker"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]

Building the WebAssembly Module Now let’s turn our Rust code into WebAssembly:

wasm-pack build --target web

This creates a pkg/ directory with all the files we need for our web app. Creating the Web Interface

Create an index.html file in your project root:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Password Strength Checker</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }

   .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }
        .input-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
            color: #555;
        }
        input[type="password"], input[type="text"] {
            width: 100%;
            padding: 12px;
            border: 2px solid #ddd;
            border-radius: 5px;
            font-size: 16px;
            box-sizing: border-box;
        }
        input:focus {
            outline: none;
            border-color: #4CAF50;
        }
        .strength-meter {
            height: 10px;
            background-color: #e0e0e0;
            border-radius: 5px;
            margin: 10px 0;
            overflow: hidden;
        }
        .strength-fill {
            height: 100%;
            transition: all 0.3s ease;
            border-radius: 5px;
        }
        .score-display {
            text-align: center;
            font-size: 18px;
            font-weight: bold;
            margin: 10px 0;
        }
        .feedback {
            background-color: #f8f9fa;
            border-left: 4px solid #007bff;
            padding: 15px;
            margin: 15px 0;
            border-radius: 0 5px 5px 0;
        }
        .criteria {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 10px;
            margin: 20px 0;
        }
        .criterion {
            display: flex;
            align-items: center;
            padding: 8px;
            background-color: #f8f9fa;
            border-radius: 5px;
        }
        .criterion.met {
            background-color: #d4edda;
            color: #155724;
        }
        .criterion .icon {
            margin-right: 8px;
            font-weight: bold;
        }
        button {
            background-color: #007bff;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin: 10px 5px;
        }
        button:hover {
            background-color: #0056b3;
        }
        .toggle-btn {
            background-color: #6c757d;
        }
        .toggle-btn:hover {
            background-color: #545b62;
        }
        @media (max-width: 480px) {
            body {
                padding: 10px;
            }
            
            .container {
                padding: 20px;
            }
            
            .criteria {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🔐 Password Strength Checker</h1>
        
        <div class="input-group">
            <label for="password">Enter your password:</label>
            <input type="password" id="password" placeholder="Type your password here...">
            <button type="button" class="toggle-btn" onclick="togglePasswordVisibility()">
                👁️ Show/Hide
            </button>
        </div>
        <div class="strength-meter">
            <div class="strength-fill" id="strengthFill"></div>
        </div>
        <div class="score-display" id="scoreDisplay">
            Score: 0/100
        </div>
        <div class="feedback" id="feedback">
            Enter a password to see its strength analysis...
        </div>
        <div class="criteria" id="criteria">
            <div class="criterion" id="lengthCriterion">
                <span class="icon">❌</span>
                <span>At least 8 characters</span>
            </div>
            <div class="criterion" id="lowercaseCriterion">
                <span class="icon">❌</span>
                <span>Lowercase letters</span>
            </div>
            <div class="criterion" id="uppercaseCriterion">
                <span class="icon">❌</span>
                <span>Uppercase letters</span>
            </div>
            <div class="criterion" id="numbersCriterion">
                <span class="icon">❌</span>
                <span>Numbers</span>
            </div>
            <div class="criterion" id="symbolsCriterion">
                <span class="icon">❌</span>
                <span>Symbols (!@#$%^&*)</span>
            </div>
        </div>
        <div style="text-align: center;">
            <button onclick="generatePassword()">🎲 Generate Strong Password</button>
        </div>
    </div>
    <script type="module">
        import init, { analyze_password, generate_password_suggestion } from './pkg/password_checker.js';
        let wasmModule;
        async function run() {
            wasmModule = await init();
            console.log('WebAssembly module loaded successfully!');
            
            // Make functions globally available
            window.analyzePassword = function(password) {
                if (!wasmModule) return null;
                return analyze_password(password);
            };
            window.generatePasswordSuggestion = function() {
                if (!wasmModule) return '';
                return generate_password_suggestion();
            };
            // Set up event listeners
            const passwordInput = document.getElementById('password');
            passwordInput.addEventListener('input', updatePasswordAnalysis);
        }
        function updatePasswordAnalysis() {
            const password = document.getElementById('password').value;
            
            if (password.length === 0) {
                resetDisplay();
                return;
            }
            const analysis = window.analyzePassword(password);
            updateDisplay(analysis);
        }
        function updateDisplay(analysis) {
            // Update score
            const scoreDisplay = document.getElementById('scoreDisplay');
            scoreDisplay.textContent = `Score: ${analysis.score}/100`;
            // Update strength meter
            const strengthFill = document.getElementById('strengthFill');
            const percentage = analysis.score;
            strengthFill.style.width = `${percentage}%`;
            
            // Color based on strength
            if (percentage < 30) {
                strengthFill.style.backgroundColor = '#dc3545'; // Red
            } else if (percentage < 60) {
                strengthFill.style.backgroundColor = '#fd7e14'; // Orange
            } else if (percentage < 80) {
                strengthFill.style.backgroundColor = '#ffc107'; // Yellow
            } else {
                strengthFill.style.backgroundColor = '#28a745'; // Green
            }
            // Update feedback
            const feedback = document.getElementById('feedback');
            feedback.textContent = analysis.feedback;
            // Update criteria
            updateCriterion('lengthCriterion', analysis.length_ok);
            updateCriterion('lowercaseCriterion', analysis.has_lowercase);
            updateCriterion('uppercaseCriterion', analysis.has_uppercase);
            updateCriterion('numbersCriterion', analysis.has_numbers);
            updateCriterion('symbolsCriterion', analysis.has_symbols);
        }
        function updateCriterion(elementId, met) {
            const element = document.getElementById(elementId);
            const icon = element.querySelector('.icon');
            
            if (met) {
                element.classList.add('met');
                icon.textContent = '✅';
            } else {
                element.classList.remove('met');
                icon.textContent = '❌';
            }
        }
        function resetDisplay() {
            document.getElementById('scoreDisplay').textContent = 'Score: 0/100';
            document.getElementById('strengthFill').style.width = '0%';
            document.getElementById('feedback').textContent = 'Enter a password to see its strength analysis...';
            
            ['lengthCriterion', 'lowercaseCriterion', 'uppercaseCriterion', 'numbersCriterion', 'symbolsCriterion']
                .forEach(id => updateCriterion(id, false));
        }
        // Global functions
        window.togglePasswordVisibility = function() {
            const passwordInput = document.getElementById('password');
            passwordInput.type = passwordInput.type === 'password' ? 'text' : 'password';
        };
        window.generatePassword = function() {
            if (!window.generatePasswordSuggestion) {
                alert('WebAssembly module not loaded yet. Please wait...');
                return;
            }
            
            const generatedPassword = window.generatePasswordSuggestion();
            const passwordInput = document.getElementById('password');
            passwordInput.value = generatedPassword;
            passwordInput.type = 'text'; // Show the generated password
            updatePasswordAnalysis();
        };
        run();
    </script>
</body>
</html>

Testing Your App To test your app, you need a local web server. Here’s the easiest way:

# If you have Python installed
python -m http.server 8000

# Or if you have Node.js
npx serve .

Open your browser and go to http://localhost:8000. You should see your password checker working in real-time!

I’m not very good with UI so I asked Claude to generate a UI for me, you can adjust it as per your with.

What Makes This Cool? Here’s what blew my mind about this approach:

  • Performance: The password analysis runs directly in WebAssembly, which is much faster than JavaScript
  • Security: No data leaves your browser — everything runs locally
  • Type Safety: Thanks to Rust’s type system, we catch errors at compile time
  • Size: The entire WebAssembly module is incredibly small
  • Portability: This same Rust code could run on servers, mobile apps, or anywhere else

What We Built Our app includes some pretty neat features:

  • Real-time password analysis as you type
  • Visual strength meter with color coding
  • Detailed feedback on what to improve
  • Smart pattern detection to catch weak passwords
  • Password generation with secure randomness
  • Mobile-friendly responsive design

Troubleshooting If something isn’t working:

  • Build errors: Make sure Rust is up to date with rustup update
  • CORS errors: Always use a web server, never open the HTML file directly
  • Module not found: Check that the pkg/ directory exists after building

Going Further This is just the beginning! You could extend this app by:

  • Adding more sophisticated entropy calculations
  • Checking passwords against breach databases
  • Creating a browser extension version
  • Adding multiple language support
  • Integrating with password managers

Resources and References

Here are some excellent resources if you want to dive deeper: Official Documentation:

API References:

Examples and Tutorials:

Wrapping Up

I’ve got to say, I’m pretty excited about what Rust and WebAssembly can do together. We built a fully functional web app without a single line of traditional backend code, and it’s fast, secure, and maintainable.

The tooling has come such a long way that you can go from zero to a working WASM app in just a few minutes. It’s definitely worth exploring if you’re looking for alternatives to the standard JavaScript stack.

What do you think? Are you interested in exploring more Rust + WebAssembly combinations? Let me know in the comments, and I’ll share more findings in my next blog post. Maybe we could tackle building a real-time image processing app or a client-side database next!

Happy coding! 🦀✨

Read the full article here: https://guycoding.medium.com/build-your-first-webassembly-app-in-rust-in-10-minutes-fb60a1740438