Introduction: REST or GraphQL — the eternal debate we'll settle in Rust
If you're writing in Rust and choosing an architecture for your API, you've probably asked yourself: what to choose — classic REST or trendy GraphQL? Both approaches have an army of fans and a mountain of haters. REST is accused of data redundancy and client-server "chattiness." GraphQL is accused of caching complexity and "hellish" database queries.
But let's face the truth: Rust is not JavaScript or Python. Its philosophy is zero-cost abstractions and maximum performance. Does this mean one of the approaches is better suited for the Rust ecosystem? Let's figure it out with real code examples.
In this article, we won't just list the pros and cons. We'll write two microservices using actix-web (REST) and async-graphql, compare their performance, development complexity, and ease of maintenance. Let's go!
Quick Primer: REST and GraphQL in 30 Seconds
REST (Representational State Transfer) is an architectural style where each resource (user, article, order) has its own URL. You retrieve data via GET, create via POST, update via PUT/PATCH. It's simple and predictable. But often you get more data than you need, or you make multiple requests to gather related entities.
GraphQL is a query language and runtime. The client decides which fields it needs. One request — and you get exactly what you asked for. But this flexibility comes at the cost of implementation complexity on the server and potential performance issues.
In Rust, both approaches are implemented at the highest level thanks to the type system and asynchronicity. But let's dig deeper.
REST in Rust: Classic with actix-web
Let's start with the familiar REST. For an example, let's take a simple blog: there are users, articles, and comments. In REST, we'll need to create several endpoints.
use actix_web::{web, App, HttpServer, Responder, HttpResponse};use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]struct Article { id: i32, title: String, content: String, author_id: i32,}
async fn get_article(path: web::Path<i32>) -> impl Responder { let article_id = path.into_inner(); // Let's pretend we're fetching the article from the DB let article = Article { id: article_id, title: "GraphQL vs REST".to_string(), content: "Detailed comparison...".to_string(), author_id: 1, }; HttpResponse::Ok().json(article)}
async fn get_articles_by_author(path: web::Path<i32>) -> impl Responder { let author_id = path.into_inner(); // List of author's articles let articles = vec![ Article { id: 1, title: "Article 1".to_string(), content: "Text...".to_string(), author_id }, Article { id: 2, title: "Article 2".to_string(), content: "Text...".to_string(), author_id }, ]; HttpResponse::Ok().json(articles)}
#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/articles/{id}", web::get().to(get_article)) .route("/authors/{id}/articles", web::get().to(get_articles_by_author)) }) .bind("127.0.0.1:8080")? .run() .await}Everything looks clean and clear. Pros of REST in Rust:
- Simplicity of caching at the HTTP level (ETag, Cache-Control).
- Transparent structure — each endpoint does one thing.
- Easy integration with any clients (curl, Postman, browser).
But there are also cons:
- The N+1 query problem. To get an article and the author, you need two requests. Or create a separate endpoint with a JOIN.
- Over-fetching.<