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), andlogin_register.rs
for handling authentication forms.Data Model: A crucial step was defining the
JobApplication
struct inmodels.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
oronsubmit
and access event properties, I needed theweb-sys
crate. A quickcargo add web-sys --features=InputEvent,Event,HtmlInputElement,SubmitEvent
in thefrontend/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?
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.
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
).Extensibility: It has a robust middleware system and integrates well with the broader Rust ecosystem.
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}
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 macrosquery!
,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 theDATABASE_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 usesargon2
'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 andHttpResponse::Ok().json(...)
responses, leveraging the power ofserde
for serialization and deserialization.CRUD for Jobs: The
add_job
andget_jobs
handlers implemented the core logic for managing job applications, usingsqlx
to interact with thejobs
table. I usedRETURNING 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
: Runstrunk serve
for the Yew app.make backend
: Runscargo run
for the Actix app.make db-init
: Runspsql
to executebackend/schema.sql
, creating theusers
andjobs
tables in my local Postgres database.make db-clean
: Truncates the tables for a fresh start.make db-users
/make db-jobs
: Simplepsql
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
, andreqwest
(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 (likePin
,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