Building a Full-Stack Web App with Yew and Actix

Development

May 2, 2025 montek.dev


It started, as many projects do but i was watching this youtube video and just wanted to build every project listed, so this is my try to the first project, a job application tracker. With an idea and a spark of curiosity. I wanted to build something practical, something mine. Simple enough, right? But I also wanted a challenge. I'd been hearing the buzz about Rust – its performance, its safety, its growing ecosystem – and thought, "I have already built a CLI, could I build a whole web application with it? Frontend and backend?"

Spoiler alert: Yes! But oh boy, what an adventure it's been. This isn't just a story about building an app; it's about diving headfirst into the Rust web ecosystem, wrestling with toolchains, discovering new frameworks, and ultimately, finding a lot of joy in the process. If you're curious about Rust for web development, grab a coffee, settle in, and let me share my journey building "ApplyForge".

Why Rust for the Web?

My background, like many web developers, is steeped in JavaScript. React, Vue, Node.js – they're powerful tools, no doubt. But Rust offered something different. The promise of near-native performance directly in the browser via WebAssembly (WASM) was incredibly appealing. Imagine complex frontend logic running lightning fast, without the typical JavaScript overhead! Plus, Rust's famous compile-time safety checks? Catching errors before they hit the browser? Yes, please! Also I was seeing most tools being built in rust for example Turbopack , and i really wanted to try building in rust.

Enter Yew. When I started looking for Rust frontend frameworks, Yew immediately stood out. Its component-based architecture felt familiar, drawing inspiration from frameworks like React and Elm. You define components as Rust structs, manage state, handle events, and render HTML using a macro html!) that feels surprisingly intuitive once you get the hang of it.

How is it different from JavaScript frameworks? Well, fundamentally, you're writing Rust! This means:

1. Strong Typing: No more undefined is not a function runtime errors (well, fewer!). The compiler is your strict but helpful friend, ensuring data flows correctly.

2. Performance: WASM is designed to be fast. While JavaScript engines are incredibly optimized, WASM often has the edge for CPU-intensive tasks.

3. Ecosystem: You leverage Rust's package manager, Cargo, and its vast library ecosystem. Need a date library? A complex algorithm? Chances are, there's a well-maintained Rust crate for it.

4. No Virtual DOM (in the traditional sense): Yew is smart about updates. It doesn't maintain a full Virtual DOM like React. Instead, it compares the new desired state with the current DOM and applies minimal changes. This can lead to very efficient rendering.

Initially there was definitely a learning curve. Rust itself has its own concepts (ownership, borrowing, lifetimes) that take time to internalize. Then, layering Yew's specific patterns on top added another dimension. But the tooling is fantastic ( still have to understand everything but yeah I can build stuff with it). trunk, the build tool and dev server commonly used with Yew, makes compiling Rust to WASM and serving the app incredibly simple trunk serve).

The "aha!" moments came when I saw my Rust code manipulating the DOM, handling button clicks, and updating the UI, all compiled down to this compact WASM binary. It felt powerful.

Weaving the Frontend

With the basics understood, I started building the ApplyForge UI.

  • Structure: I organized the code logically: main.rs for the entry point, app.rs for the main application component holding the state (like the list of job applications), and login_register.rs for handling authentication forms.

  • Data Model: A crucial step was defining the JobApplication struct in models.rs. Having a clear, typed structure for the data from the start is a huge benefit of using Rust.

  • Forms and Events: Building forms required handling input changes and submission events. This led me to my first dependency snag. To interact directly with DOM events like oninput or onsubmit and access event properties, I needed the web-sys crate. A quick cargo add web-sys --features=InputEvent,Event,HtmlInputElement,SubmitEvent in the frontend/Cargo.toml and I was back in business.

  • Styling : Ah, styling. I wanted a clean, modern dark theme. I started by ditching the default Yew styles and writing custom SCSS using modern CSS features like oklch for colors. Then, I got ambitious: "Let's use Tailwind CSS!" I love Tailwind's utility-first approach. However, integrating it with the Trunk/Sass setup proved tricky. The standard @import "tailwindcss"; (tailwind v4) directive didn't play nicely with the way Trunk processes Sass. After wrestling with it for a bit, I decided to stick with my custom SCSS for simplicity. The result is still a nice dark theme I'm happy with, even if it's not Tailwind.

  • UI Elements: I added buttons for future features like CSV export. For now, they're just placeholders, but having them in the UI helps visualize the end goal.

The frontend was taking shape. It could display mock data, handle form inputs, and looked decent. But it was lonely. It needed data, real data, from a backend.

The Backend

I knew I wanted a Rust backend to complete the full-stack Rust experience. But which framework? I hadn't initially researched this as much as the frontend. After some digging, Actix Web emerged as a popular and highly performant choice.

Why Actix?

  1. Performance: It's consistently ranked among the fastest web frameworks available in any language, thanks to its asynchronous nature built on Tokio and Rust's efficiency.

  2. Asynchronous: Modern web apps need to handle many connections concurrently without blocking. Actix is async-first, making this natural (once you grasp Rust's async/await).

  3. Extensibility: It has a robust middleware system and integrates well with the broader Rust ecosystem.

  4. Type Safety: Just like the frontend, the backend benefits immensely from Rust's compile-time checks.

Setting up a basic Actix project was straightforward: add actix-web to backend/Cargo.toml, create a main.rs, and write a simple async fn main() function using the #[actix_web::main] macro. A basic "Hello World" endpoint was up and running in minutes.

1// backend/src/main.rs (simplified)
2
3use actix_web::{get, App, HttpResponse, HttpServer, Responder};
4
5#[get("/api/hello")]
6
7async fn hello() -> impl Responder {
8
9    HttpResponse::Ok().json("Hello from Actix Web!")
10
11}
12
13#[actix_web::main]
14
15async fn main() -> std::io::Result<()> {
16
17    HttpServer::new(|| {
18
19        App::new().service(hello)
20
21    })
22
23    .bind(("0.0.0.0", 8080))?
24
25    .run()
26
27    .await
28
29}
rust

Easy! Now, for the real work: authentication, database interaction, and the jobs API.

Building the API

This is where things got more involved, but also incredibly rewarding.

  • Database Choice: I opted for PostgreSQL. It's robust, feature-rich, and well-supported in the Rust ecosystem.

  • SQLx: For database interaction, I chose sqlx. It's an async SQL toolkit thats modern and, crucially, offers compile-time query checking via its macros query!, query_as!). This means the Rust compiler connects to your database during compilation to verify your SQL syntax and type mappings. Mind. Blown. No more runtime SQL errors because of a typo!

  • Connection Pooling: Setting up a connection pool using sqlx::postgres::PgPoolOptions was essential for handling multiple database requests efficiently.

  • Environment Variables: Using the dotenv crate allowed me to manage the DATABASE_URL easily, keeping credentials out of the code.

  • API Endpoints: I defined routes using Actix macros #[get(...)], #[post(...)]) for:

    • /api/register: Creates a new user.

    • /api/login: Authenticates an existing user.

    • /api/jobs: Adds a new job application (POST).

    • /api/jobs/{username}: Gets all jobs for a user (GET).

    • /api/jobs/{id}: Delete a specific user job

  • Authentication: Security first! I used the argon2 crate to hash user passwords securely during registration. The login endpoint fetches the stored hash and uses argon2's verification functions to check the provided password. Storing plain text passwords is a huge no-no!

  • Handling JSON: Actix makes working with JSON payloads seamless using web::Json<T> extractors and HttpResponse::Ok().json(...) responses, leveraging the power of serde for serialization and deserialization.

  • CRUD for Jobs: The add_job and get_jobs handlers implemented the core logic for managing job applications, using sqlx to interact with the jobs table. I used RETURNING id in the INSERT query to get the newly created job's ID back immediately.

  • CORS: Since the frontend (running on port 3000 or 80) would be making requests to the backend (running on port 8080), Cross-Origin Resource Sharing (CORS) needed to be configured. The actix-cors crate made this simple – Cors::permissive() was enough for development.

The backend felt solid. The compile-time checked SQL with sqlx was a game-changer, providing confidence I rarely felt with dynamically typed languages and ORMs.

Local Development

Okay, I had a frontend and a backend. How to run them smoothly locally? I turned to a trusty Makefile.

I added targets for:

  • make frontend: Runs trunk serve for the Yew app.

  • make backend: Runs cargo run for the Actix app.

  • make db-init: Runs psql to execute backend/schema.sql, creating the users and jobs tables in my local Postgres database.

  • make db-clean: Truncates the tables for a fresh start.

  • make db-users / make db-jobs: Simple psql commands to quickly view table contents in the terminal.

This worked great for local iteration. However, I hit a snag with sqlx. Because its macros check queries at compile time, cargo run (or cargo build) needed access to the database. This meant ensuring the DATABASE_URL environment variable was set correctly before running make backend. I handled this by creating a backend/.env file, which dotenv automatically picks up. Problem solved!

For a more visual way to inspect the database than psql in the terminal, I knew I could use GUI tools like Postico or TablePlus.

Error I faced

Early on, I had issues compiling for WASM wasm32-unknown-unknown). It turned out my initial Rust install via Homebrew was conflicting with rustup (the official Rust toolchain manager). The fix was simple but crucial: uninstall the Homebrew version, install via rustup, and explicitly add the WASM target: rustup target add wasm32-unknown-unknown. Lesson learned: stick to rustup for managing Rust installations!

Reflections on the Rust Web Journey

So, after all that, what's the verdict on full-stack Rust?

The Good:

  • Performance: Both the Actix backend and the Yew WASM frontend feel incredibly snappy.

  • Safety: The compiler caught so many potential errors that would have been runtime bugs in other languages. This is especially true for sqlx's compile-time query checks.

  • Ecosystem Maturity: For web development, the Rust ecosystem is surprisingly robust. Crates like serde, tokio, actix-web, yew, sqlx, argon2, and reqwest (for frontend HTTP requests) cover most needs.

  • The Joy of Rust: Once you get past the initial learning curve, writing Rust is genuinely enjoyable. The explicitness, the powerful type system, and the focus on correctness lead to code I feel confident in.

The Challenges:

  • Learning Curve: Rust isn't the easiest language to pick up, and adding web framework concepts on top takes time.

  • Compile Times: While improving, Rust compile times can still be slower than interpreted languages, especially for larger projects.

  • Tooling Quirks: As seen with the Tailwind/Trunk issue and the SQLx offline mode dance, sometimes you hit rough edges where tools interact in unexpected ways. Debugging these requires patience.

  • Async Rust: async/await is powerful but has its own complexities (like Pin, Send, Sync) that can be initially confusing.

Was it worth it? Absolutely. Building ApplyForge has been an incredible learning experience. I feel like I've leveled up not just in Rust, but in my understanding of web fundamentals, build systems, and containerization. Yew and Actix feel like a powerful combination, offering a type-safe, performant alternative to traditional web stacks.

What's next for ApplyForge? Implementing the actual CSV export and OAuth logic, adding more features to the job tracker, refining the UI, and maybe even deploying it somewhere!

If you're considering Rust for your next web project, I wholeheartedly encourage you to give it a try. Be prepared for a learning curve, embrace the compiler's guidance, and don't be afraid to dive deep when troubleshooting. The performance, safety, and sheer satisfaction of building with Rust are well worth the journey. Happy coding!

Github code: https://github.com/Montekkundan/applyforge


May 2, 2025 montek.dev