Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
172: Building a GraphQL Server with async-graphql
When you first dive into async-graphql, it feels like magic. You define a few structs, slap some #[Object] macros on them, and suddenly you have a fully typed schema. It's tempting to treat your GraphQL resolvers like simple API endpoints—just a place to fetch some data and return it. But there's a trap here that I've seen countless engineers fall into: the N+1 query problem. If you aren't careful, a single request from a client can accidentally trigger hundreds of database calls, effectively DDOS-ing your own backend.
The "Just Fetch It" Approach
Imagine we're building a digital library. We have Books and Authors. In a naive implementation, you'd probably write your Book object like this:
#[Object]
impl Book {
async fn author(&self, ctx: &Context<'_>) -> Result<Author> {
let db = ctx.data<Database>()?;
// The naive way: fetch the author for every single book
let author = db.get_author_by_id(self.author_id).await?;
Ok(author)
}
}
On the surface, this looks clean. It's intuitive. If the user asks for the author of a book, you go get the author. But here's where it breaks: what happens when a client queries for the top 50 most popular books, including their authors? GraphQL will call the author resolver 50 times. That's one query for the list of books, plus 50 separate queries for authors. That's 51 round-trips to your database for a single HTTP request. It works fine in development with three rows of data, but it'll crawl once you hit production scale.
Batching with DataLoaders
To fix this, we need to stop thinking about "fetching one" and start thinking about "fetching many." async-graphql provides a DataLoader trait specifically for this. Instead of the resolver immediately hitting the database, it submits a "request" for an ID to a loader. The loader then waits for a tiny tick of the event loop, collects all the IDs requested across all concurrent resolvers, and fetches them in a single SELECT ... WHERE id IN (...) query.
Here is how I'd actually structure this. First, we define the loader:
struct AuthorLoader { db: Database }
#[async_trait::async_trait]
impl Loader<UserId, Author> for AuthorLoader {
async fn load(&self, keys: &[UserId]) -> Result<HashMap<UserId, Author>> {
// One query to rule them all
let authors = self.db.get_authors_by_ids(keys).await?;
Ok(authors.into_iter().map(|a| (a.id, a)).collect())
}
}
Now, our Book resolver changes. Instead of talking to the database, it talks to the loader:
#[Object]
impl Book {
async fn author(&self, ctx: &Context<'_>) -> Result<Author> {
let loader = ctx.data<DataLoader<AuthorLoader>>()?;
// This doesn't hit the DB immediately; it schedules a batch load
let author = loader.load_one(self.author_id).await?;
author.ok_or_else(|| Error::new("Author not found"))
}
}
The Cost of Efficiency
You might be wondering if this is overkill. Is the extra boilerplate of a Loader struct worth it? In my experience, yes—absolutely. The trade-off is a slight increase in architectural complexity for a massive gain in stability. You're moving from O(N) database pressure to O(1) per field level.
One thing to watch out for: DataLoaders are cached for the duration of a single request. This is usually what you want, as it prevents fetching the same author twice if two different books were written by the same person. However, if you're doing heavy mutations in the same request and expecting the loader to reflect those changes immediately, you'll need to be mindful of how you're managing that cache. For 95% of read-heavy GraphQL APIs, the DataLoader pattern is the non-negotiable way to build.
📋 Practical Task
Implementing a Batched Review Loader for the Library API
You have a Book object that currently fetches its reviews one by one, causing a massive performance hit when listing multiple books. Your task is to implement a ReviewLoader to batch these requests.
Requirements:
- Create a
ReviewLoaderstruct that implementsasync_graphql::DataLoader. - The
loadmethod should take a slice ofBookIds and return aHashMap<BookId, Vec<Review>>. - Assume the database has a method
db.get_reviews_for_books(ids: &[BookId])that returns aVec<Review>. You will need to group these reviews by theirbook_idinside the loader. - Update the
Bookobject'sreviewsresolver to use theDataLoaderinstead of calling the database directly.
There are no comments for now.