My journey with Rust began quite a while ago. I’ve always wanted to learn a low-level language, something that would allow me to build desktop applications and leverage advanced features like multi-threading, asynchronous programming, and manual memory management.
However, my background is entirely in web development. I started my career on the front end, working with Bootstrap, Sass, and the usual stack. Later, I shifted paths and became a backend developer, working exclusively with PHP. Despite my interest in Rust, I never really focused on it, instead, I concentrated on mastering PHP and the fundamentals of software engineering. My career path consistently led back to the web, so my scope remained somewhat limited.
Even so, I had the desire to learn a language where I could tackle more complex problems. At the time, I also found myself wishing for features in PHP that weren’t yet available, like robust generics and strict type hinting (at the time!). While PHP has evolved tremendously and is now a breeze to work with, it still lacks some of the low-level control I was looking for.
Like many people, I work a full eight-hour day. I only had a few hours left in the evening to study, but I’d often come home exhausted, wanting only to rest and spend time with my spouse and my dogs. This cycle continued for years. I struggled to set aside dedicated time for a low-level language because, frankly, life gets in the way.
My Rust Timeline
| Year | Milestone | What happened |
|---|---|---|
| 2020 | First contact | Read “The Book”, started Rustlings, created a exercises repo |
| 2022 | Real-world project | Built a Sonar runner in Rust, adopted by my team |
| 2024 | Final paper | Built a full-stack app (SvelteJS + Rust/Axum), scored 9.5 |
| 2024/25 | Personal project | Started to build a personal project (linux status bar) |
First Try
In 2020, I finally took the plunge. After hearing about Rust, I started reading “The Book,” joined the subreddit, and began working through the Rustlings exercises. I was genuinely enjoying myself and felt very focused.
I created a rust-exercises repository to track my progress. Every day after work, I would study. But as often happens, life got busy, and my momentum faded. It was a natural drift, but looking back a few weeks later, I felt disappointed. I truly felt that Rust had a special place in my career goals, yet I had let it slip.
Lesson learned
Starting with only tutorials and exercises wasn’t enough to keep me motivated. Without a real project to anchor my learning, it was easy to lose momentum.
Second Try
In 2022, I changed companies. I was working with a great team of talented developers and felt a renewed urge to learn. I started over: the book, the subreddit, the articles. But this time, something was different. I had the opportunity to work on a team project and decided to use Rust as the main language.
The project was a Sonar runner designed to identify packages in our monolith, run tests and coverage via PHPUnit, and upload the report using a Docker image.
The experience was a blast. I finally started to grasp types, traits, and structs. I also encountered the topics that almost broke my head: multi-threading and async. Despite being a beginner, the project was a success and was actually adopted by the team. Building a real-world application taught me more than any tutorial could.
What made the difference
Having a real problem to solve changed everything. When you need your code to actually work for your team, you push through the frustration instead of giving up.
Final Paper
For my final paper, I built a pet management application. I worked on it for six months. It consists of a SvelteJS front end and a Rust backend.
To be honest, it was a stressful experience. Coming from a PHP background where building APIs with frameworks like Laravel is incredibly fast, working with Rust’s Axum was a steep learning curve. Trying to understand complex types while attempting rapid prototyping felt nearly impossible at times.
I remember getting frustrated frequently. There were moments when I seriously questioned if using Rust for my final paper was the right choice. Despite the frustration, it was a labor of love. I had to learn how to build an application without relying on the Object-Oriented patterns I use every day at work. As the deadline approached, I got anxious about whether I’d finish in time.
In the end, it all went well. I managed to cover almost the entire application with integration tests and received a 9.5 as my final grade.
The hard truth about switching paradigms
Coming from PHP/Laravel where you can scaffold an entire API in minutes, Rust felt painfully slow at first. But the compiler errors that frustrated me were also teaching me to write more correct code from the start.
Inside the Final Paper
Since I briefly mentioned my final paper above, I want to dive a bit deeper into what I actually built and the Rust features I used along the way. If you’re considering Rust for a web project, this might give you a realistic picture of what to expect.
The application is called Fredy, a pet management system. Think of it as a place where pet owners can track their pets’ health records: weight history, allergies, medications, vaccines, and vet appointments. The backend is a REST API built with Axum, using PostgreSQL as the database and SQLx for queries. The whole thing runs on Tokio, Rust’s async runtime.
Thinking in types, not objects
The biggest mental shift coming from PHP was moving away from classes and inheritance. In Rust, you model your domain with structs and enums, and you define behavior with traits. There are no classes. There is no extends. At first, this felt limiting. How do I share behavior between different parts of my application? The answer is traits. For example, every route module in the app implements a BaseRouter trait, which defines a single method: register_routes(). It’s a simple contract. Each module (pets, users, auth) implements it and returns its own set of routes. No abstract base class needed.
I also used traits to extend external types, something PHP simply can’t do. I wrote an extension trait for SQLx’s QueryBuilder that added a .paginate() method. This let me chain pagination directly into my database queries, which felt incredibly clean.
The compiler is your strictest code reviewer
In Rust, if it compiles, there’s a very good chance it works. The compiler forces you to handle every possible error, every edge case. At first, I was fighting it constantly. Why won’t you just let me unwrap this? Why do I need to handle this case?
But over time, I started to appreciate it. My error handling became genuinely good, not because I’m disciplined, but because the compiler gave me no other choice. I built a layered error system using the thiserror crate: domain-specific errors like AuthError and DatabaseError get wrapped into a ServerError enum, which then converts into the appropriate HTTP response through Rust’s From trait and pattern matching. Every error variant maps to a specific status code and message. No unhandled exceptions sneaking into production.
Embrace the compiler
Stop fighting it. Every error the Rust compiler throws at you is a bug it’s catching before your users do. The frustration you feel is the cost of correctness, and it’s worth paying.
Async all the way down
The entire application is async. Every route handler, every database query, every WebSocket message. Tokio handles the runtime, and Axum is built on top of it. If you’ve used async/await in JavaScript, the syntax will feel familiar. But Rust adds ownership rules on top of async, which can be mind-bending at first.
One feature I’m particularly proud of is the WebSocket system for real-time notifications. It uses Tokio’s broadcast channels to send messages to connected clients, filtering by user ID so each person only receives their own notifications. Getting the types right for this, Arc<RwLock<HashMap<ConnectionId, Client>>>, took me longer than I’d like to admit. But once it compiled, it just worked.
I also built a background job scheduler that runs in its own thread with its own Tokio runtime. It handles things like sending birthday reminders for pets and vaccination notifications. Cron expressions define the schedule, and each job spawns an async task.
SQLx: queries checked at compile time
This was one of my favorite discoveries. SQLx doesn’t just run your SQL queries, it verifies them against your actual database schema at compile time. If you write a query that references a column that doesn’t exist, or if the types don’t match, it won’t compile. Coming from PHP where a typo in a query string might only surface in production, this felt like a superpower.
I combined this with Rust’s FromRow derive macro to automatically map database rows into structs. No manual mapping, no ORMs generating queries behind your back. You write the SQL, Rust checks it, and the result is a typed struct. It’s honest and transparent.
Patterns that clicked
A few Rust patterns became essential throughout the project:
- From and Into conversions: I used these everywhere to convert between database models and API resources. Instead of writing transformation methods, you implement the From trait once, and then
.into()just works. It’s elegant and feels very natural once you get used to it. - Type aliases: Rust lets you name complex types. Instead of writing
Arc<RwLock<HashMap<WebSocketConnectionId, WebSocketClient>>>everywhere, I defined typeConnections = Arc<RwLock<...>>. This made the code significantly more readable. - Custom extractors: Axum lets you define your own extractors. I built a
ValidatedData<T>extractor that deserializes the request body and validates it in a single step. If validation fails, it returns a proper error response automatically. This removed a ton of boilerplate from my route handlers. - The newtype pattern: Wrapping primitive types in single-field structs to give them meaning. A
WebSocketConnectionId(u64)is not just a number, it’s a connection ID. The compiler won’t let you accidentally pass a random u64 where a connection ID is expected. Zero runtime cost, maximum safety.
Testing without a framework
Testing is built into the language. You write #[test] (or #[tokio::test] for async tests), and cargo test runs them. No external framework required.
For my integration tests, each test spins up a fresh PostgreSQL database with a random name, runs all migrations, loads fixtures, and tears everything down afterward. I used the test-context crate to handle setup and teardown, and wrote helper methods like router.logged_as_admin() that inject authentication into test requests. The result is clean, readable tests that actually hit the database, no mocking needed.
Testing in Rust is refreshingly simple
You don’t need a testing framework, a DI container, or mock libraries. The language’s type system and the #[test] attribute give you everything you need. My integration tests were more reliable than any I’d written in PHP, and they ran faster too.
Was it worth it?
Absolutely, but I won’t pretend it was easy. The project has around 5,000 lines of Rust across 69 files, with 18 database migrations, 30+ API endpoints, and 20 test files. In Laravel, I could have built the same thing in a fraction of the time. But I wouldn’t have learned half as much.
Rust forced me to think about things I’d never considered in PHP: who owns this data? How long does this reference live? What happens when this operation fails? These aren’t just Rust questions, they’re software engineering questions. Rust just makes them impossible to ignore.
What’s next?
My latest Rust project is called Fridarchy, a custom Wayland status bar for Hyprland. Think of it as building something like Waybar or Polybar from scratch, entirely in Rust. This is by far the most ambitious thing I’ve attempted in the language, and the one that has tested my patience the most.
The idea came from a simple place: I wanted a bar that worked exactly the way I wanted. Not close enough, not “good enough with workarounds.” Exactly. So I decided to build it myself. Six thousand lines of Rust across 50+ files, 12 widget types, integrations with D-Bus, Wayland protocols, input devices, HTTP APIs, and an audio visualizer.
GTK, Wayland, and the ecosystem that fights you
The first challenge was the GUI layer. I chose GTK4 with Relm4, an Elm-inspired framework for building reactive UIs in Rust. The documentation for Relm4 is decent, but the moment you need to do something specific to Wayland, like positioning your bar on a layer shell, you enter territory where Stack Overflow has zero answers and the best resource is reading the C source code of other bars. The gtk4-layer-shell crate is the bridge between GTK and the Wayland layer shell protocol, and getting it to cooperate with Relm4’s component model was not straightforward at all.
I spent an embarrassing amount of time just trying to dynamically load widgets into the bar. The Relm4 component lifecycle, with its init/update/view pattern and the view! macro, requires you to think about UI construction in a way that is fundamentally different from anything in the web world. There’s no virtual DOM. There’s no JSX. You’re building GTK widget trees through macros and fighting the borrow checker at every step.
Async meets GTK: two runtimes, one headache
Fridarchy runs on Tokio for all its background work: fetching weather data from wttr.in, listening to MPRIS events for the music player widget, polling system info for CPU and memory stats, reading keyboard lock states through libinput. But GTK has its own main loop. You can’t just .await inside a GTK callback. These two worlds don’t naturally talk to each other.
Relm4 provides a commands mechanism to bridge this gap, and I leaned on it heavily. It was the day I learned that blocking the GTK main thread with a synchronous call will lock your entire desktop bar. Not just the widget. The entire bar. And since the bar runs on a Wayland layer shell, you can’t even click through it to close the terminal. You have to SSH into your own machine or switch to a TTY. Fun times.
The config system rewrite cycle
I went through three configuration approaches. First TOML with the Figment crate, then KDL with Knuffel crate, and then KDL with Knus when Knuffel got deprecated. Each migration required touching nearly every file in the project.
This is a reality of building on the Rust ecosystem: crates get deprecated, APIs change, and the community is still young enough that the “standard” way of doing things shifts under your feet. In PHP, Laravel’s config system has been stable for years. In Rust, I rewrote mine three times in two months.
Services, providers, and the trait addiction
One thing I’m genuinely proud of is the service architecture. Every external integration (weather, music, keyboard, audio, compositor) is abstracted behind an async trait. The weather widget doesn’t know about wttr.in. The keyboard widget doesn’t know about libinput. They talk to traits, and the concrete implementation is wired through a ServiceContainer using OnceCell for lazy initialization.
This pattern came naturally from what I learned in my final paper. But the journey getting there was messy. I first built the keyboard widget using the evdev crate for raw input device access, realized it was the wrong abstraction, and replaced the entire thing with libinput. The commit touched 36 files with 381 insertions and 455 deletions. That’s not a refactor. That’s starting over.
The Liquid templating gamble
One of the more unusual decisions was embedding the Liquid templating engine so users can format widget output with templates like {{ title }} or {{ cpu_usage_percent | datasize }}. I also wrote custom Liquid filters for data sizes, time formatting, and conditional rendering, plus a custom {% env VAR %} tag for environment variable injection.
This was a challenge because Liquid is a Ruby-born templating language, and the Rust implementation doesn’t have the most welcoming API for extending it. Writing custom filters and tags meant implementing traits with specific associated types and wrestling with liquid-core’s value system. But the result is that every widget in Fridarchy supports the same powerful formatting syntax, and users can customize their bar output without touching Rust code.
The six-month gap
There’s an intense burst of commits from August to September 2024, 168 commits in about seven weeks. Then silence. The next commits appear in March 2025, six months later, just to update deprecated dependencies.
I know this pattern well by now. It’s the same cycle from 2020 and 2022. Life, work, fatigue. The difference is that this time, the project is real. It runs on my machine every day. I use it. When I come back to it, the codebase is waiting for me, and the compiler still remembers every contract I made. Rust codebases age well because the type system acts as documentation. When I opened the project after six months, the code was still readable, and the compiler guided me through every dependency update.
What’s actually next
Fridarchy is far from done. The volume widget is half-built (I went from WirePlumber to PulseAudio mid-implementation, another rewrite story). The Cava audio visualizer widget exists but isn’t complete. There are features I want to add: tray support, more compositor backends beyond Hyprland, maybe even a configuration GUI.
But more importantly, this project represents the point where Rust stopped being something I study and became something I use. Not for a grade. Not for a team. For myself. I don’t know when the next burst of commits will come. But I know the project will still compile when it does.
Resources
Throughout this endeavor, I consumed a lot of free resources. Here are the ones that helped me the most:
I love reading deep-dive discussions on specific topics. The Rust subreddit was my go-to place. I mostly lurk, but you’d be amazed at the knowledge you can find in a two-year-old comment from an anonymous user.
Trevor Sullivan
This YouTube channel was a lifesaver. He covers basic and intermediate Rust topics in a way that is very easy to follow.
Jon Gjengset
Jon focuses on more advanced topics, and his videos are quite long. However, even for a beginner, his “Crust of Rust” series is invaluable. The first video I watched was his explanation of building a fast concurrent database: Watch here.
Jeremy Chone
For my final paper, Jeremy’s “Rust Axum Full Course” was essential. It helped me bridge the gap between theory and building a functional web server: Watch here.
Other resources
- Compiler Explorer - Explore and inspect a compiled Rust program
- Rust Playground - Run Rust code directly in the browser
- Rust by Example - Learn through practical, real-world examples
- Clippy Lints - Documentation for rust-clippy linting rules
- Microsoft Rust Path - Free introductory training by Microsoft
- Easy Rust - Learn Rust using simple, easy-to-understand English