As someone who loves games, books, and movies, I've always wanted a single place to track what I'm watching, reading, and playing. While many apps do this for one type of media, I wanted one app to rule them all. This led me to create Completionist API, a backend service designed to be the central hub for all my media-tracking needs. I chose to build it in Rust, and this post explores the journey, the architecture, and the key features of the project.
Why Rust and Axum?
For a backend service that needs to be reliable, performant, and safe, Rust was a natural choice. Its strong type system and ownership model eliminate entire classes of bugs at compile time. I paired it with Axum, a modern and ergonomic web framework built by the tokio team. Axum's modularity, powerful extractor system, and seamless integration with the async ecosystem made it a joy to work with.
The Core Architecture: Layers of Responsibility
I designed the API with a clear, layered architecture to keep the code organized and maintainable:
- Routes (routes/): Defines the API endpoints, handles incoming HTTP requests, and uses Axum extractors to parse data like JSON payloads and path parameters.
- Database (db/): Contains all the database logic. By using sqlx, I could write raw SQL queries that are checked against the actual database schema at compile time.
- Services (services/): Manages business logic and integration with external APIs. For example, it fetches movie data from OMDb or manga details from Jikan.
- Models (models/): Defines the data structures used throughout the application, such as User, MediaItem, and UserListItem.
- Authentication (auth/): A dedicated module for handling security.
Feature Highlights
Secure and Modern Authentication
No API is complete without secure authentication. The process is twofold:
- Registration: When a user registers, their password isn't stored directly. Instead, I use the argon2 crate to produce a strong, salted hash of the password.
- Login & Authorization: On login, the provided password is hashed and compared to the stored hash. If they match, a JSON Web Token (JWT) is generated.
A Smart Database that Evolves
The application is backed by a PostgreSQL database, run inside a Docker container for easy setup. The database schema started simple but grew as I added features. This evolution is managed by sqlx-cli migrations.
One of the most interesting database functions is find_or_create_media_item. When a user adds a movie from OMDb, for example, the system first checks if a media_items record with source = 'OMDB' and the corresponding external_id already exists. If it does, it uses the existing record. If not, it creates a new one.
Self-Documenting with OpenAPI
Writing API documentation by hand is tedious and error-prone. To solve this, I used the incredible utoipa crate. By adding a few macros to my route handlers and data models, it automatically generates a complete OpenAPI v3 (Swagger) specification.
The application serves a beautiful Swagger UI, allowing anyone (including my future self or a potential frontend developer) to explore the API's endpoints, see the required data structures, and even test the endpoints directly from the browser.
Final Thoughts
Building the Completionist API has been a fantastic learning experience. It solidified my understanding of Rust for backend development and gave me hands-on experience with the entire lifecycle of a modern web service—from database design and secure authentication to external API integration and documentation.