GraphQL Guide
This guide explains how to use GraphQL with the Armature framework, inspired by NestJS's @nestjs/graphql.
Overview
Armature provides optional GraphQL support through the armature-graphql module, built on top of async-graphql. This integration enables you to build type-safe GraphQL APIs with full dependency injection support and programmatic schema generation.
Features
โ Type-Safe Schema - Compile-time verified GraphQL types โ Programmatic Schema - Build schemas programmatically like NestJS โ Queries & Mutations - Full CRUD operations support โ Subscriptions - Real-time GraphQL subscriptions โ DI Integration - Services injected into resolvers โ Decorator-Style - Rust procedural macros for clean syntax โ GraphiQL/Playground - Built-in query interface โ Schema Introspection - Automatic API documentation
Installation
Add the GraphQL feature to your Cargo.toml:
[dependencies]
armature-framework = { version = "0.1", features = ["graphql"] }
armature-graphql = "0.1"
Quick Start
1. Define Your Types
use armature_graphql::{SimpleObject, ID};
#[derive(SimpleObject)]
struct Book {
id: ID,
title: String,
author: String,
year: i32,
}
2. Create Query Root
use armature_graphql::Object;
struct QueryRoot {
book_service: BookService,
}
#[Object]
impl QueryRoot {
async fn books(&self) -> Vec<Book> {
self.book_service.get_all_books()
}
async fn book(&self, id: ID) -> Option<Book> {
self.book_service.get_book_by_id(&id)
}
}
3. Create Mutation Root
struct MutationRoot {
book_service: BookService,
}
#[Object]
impl MutationRoot {
async fn create_book(&self, title: String, author: String) -> Book {
self.book_service.create_book(title, author)
}
}
4. Build Schema Programmatically (NestJS-style)
use armature_graphql::{ProgrammaticSchemaBuilder, EmptySubscription};
// Using the programmatic builder
let schema = ProgrammaticSchemaBuilder::new()
.query(QueryRoot { book_service: book_service.clone() })
.mutation(MutationRoot { book_service: book_service.clone() })
.subscription(EmptySubscription)
.add_service(book_service) // Add to schema context
.build();
// Or use the standard Schema::build
let schema = Schema::build(
QueryRoot { book_service },
MutationRoot { book_service },
EmptySubscription
).finish();
5. Create GraphQL Endpoint
router.add_route(Route {
method: HttpMethod::POST,
path: "/graphql".to_string(),
handler: Arc::new(move |req| {
let schema = schema.clone();
Box::pin(async move {
// Handle GraphQL request
let gql_req: GraphQLRequest = req.json()?;
let request = async_graphql::Request::new(gql_req.query);
let response = schema.execute(request).await;
let json = serde_json::to_value(&response)?;
HttpResponse::ok().with_json(&json)
})
}),
});
Type System
Simple Objects
For simple data types:
#[derive(SimpleObject)]
struct User {
id: ID,
name: String,
email: String,
age: i32,
}
Complex Objects with Resolvers
For types with computed fields:
struct User {
id: ID,
name: String,
}
#[Object]
impl User {
// Simple field
async fn id(&self) -> &ID {
&self.id
}
// Computed field
async fn full_name(&self, format: Option<String>) -> String {
match format.as_deref() {
Some("upper") => self.name.to_uppercase(),
Some("lower") => self.name.to_lowercase(),
_ => self.name.clone(),
}
}
// Field with service injection (via context)
async fn posts(&self, ctx: &Context<'_>) -> Vec<Post> {
let service = ctx.data::<PostService>().unwrap();
service.get_user_posts(&self.id)
}
}
Enums
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum Role {
Admin,
User,
Guest,
}
Unions
#[derive(Union)]
enum SearchResult {
User(User),
Post(Post),
Comment(Comment),
}
Input Objects
For mutation arguments:
#[derive(InputObject)]
struct CreateUserInput {
name: String,
email: String,
age: Option<i32>,
}
#[Object]
impl MutationRoot {
async fn create_user(&self, input: CreateUserInput) -> User {
// Create user from input
}
}
Queries
Basic Query
#[Object]
impl QueryRoot {
async fn hello(&self) -> &str {
"Hello, World!"
}
async fn users(&self) -> Vec<User> {
self.user_service.get_all()
}
}
GraphQL:
query {
hello
users {
id
name
}
}
Query with Arguments
#[Object]
impl QueryRoot {
async fn user(&self, id: ID) -> Result<User> {
self.user_service
.get_by_id(&id)
.ok_or("User not found".into())
}
async fn search_users(
&self,
query: String,
limit: Option<i32>,
) -> Vec<User> {
self.user_service.search(&query, limit.unwrap_or(10))
}
}
GraphQL:
query {
user(id: "123") {
id
name
}
searchUsers(query: "john", limit: 5) {
id
name
}
}
Nested Queries
struct User {
id: ID,
name: String,
}
#[Object]
impl User {
async fn id(&self) -> &ID { &self.id }
async fn name(&self) -> &str { &self.name }
async fn posts(&self, ctx: &Context<'_>) -> Vec<Post> {
ctx.data::<PostService>()
.unwrap()
.get_user_posts(&self.id)
}
}
struct Post {
id: ID,
title: String,
}
#[Object]
impl Post {
async fn id(&self) -> &ID { &self.id }
async fn title(&self) -> &str { &self.title }
async fn author(&self, ctx: &Context<'_>) -> User {
ctx.data::<UserService>()
.unwrap()
.get_by_id(&self.author_id)
}
}
GraphQL:
query {
user(id: "123") {
name
posts {
title
author {
name
}
}
}
}
Mutations
Basic Mutations
#[Object]
impl MutationRoot {
async fn create_user(&self, name: String, email: String) -> User {
self.user_service.create(name, email)
}
async fn update_user(&self, id: ID, name: String) -> Result<User> {
self.user_service
.update(&id, name)
.ok_or("User not found".into())
}
async fn delete_user(&self, id: ID) -> bool {
self.user_service.delete(&id)
}
}
GraphQL:
mutation {
createUser(name: "John", email: "john@example.com") {
id
name
}
updateUser(id: "123", name: "Jane") {
id
name
}
deleteUser(id: "123")
}
Mutations with Input Objects
#[derive(InputObject)]
struct CreatePostInput {
title: String,
content: String,
author_id: ID,
tags: Vec<String>,
}
#[Object]
impl MutationRoot {
async fn create_post(&self, input: CreatePostInput) -> Post {
self.post_service.create(input)
}
}
GraphQL:
mutation {
createPost(input: {
title: "My Post"
content: "Post content"
authorId: "123"
tags: ["rust", "graphql"]
}) {
id
title
}
}
Subscriptions
Real-time Updates
use armature_graphql::Subscription;
use futures_util::Stream;
struct SubscriptionRoot;
#[Subscription]
impl SubscriptionRoot {
async fn books(&self) -> impl Stream<Item = Book> {
// Return a stream of books
async_stream::stream! {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
yield Book {
id: ID::from("1"),
title: "New Book".to_string(),
author: "Author".to_string(),
year: 2024,
};
}
}
}
}
GraphQL:
subscription {
books {
id
title
}
}
Programmatic Schema Building (NestJS-style)
Armature provides a ProgrammaticSchemaBuilder for building GraphQL schemas programmatically, similar to NestJS's approach:
use armature_graphql::ProgrammaticSchemaBuilder;
// Create services
let user_service = UserService::default();
let post_service = PostService::default();
// Create resolvers with injected services
let query = QueryRoot {
user_service: user_service.clone(),
post_service: post_service.clone(),
};
let mutation = MutationRoot {
user_service: user_service.clone(),
post_service: post_service.clone(),
};
// Build schema programmatically
let schema = ProgrammaticSchemaBuilder::new()
.query(query)
.mutation(mutation)
.subscription(EmptySubscription)
.add_service(user_service) // Add to context
.add_service(post_service) // Add to context
.build();
Comparison with NestJS
NestJS (@nestjs/graphql):
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: true,
}),
],
providers: [UserService, UserResolver],
})
export class AppModule {}
@Resolver(() => User)
export class UserResolver {
constructor(private userService: UserService) {}
@Query(() => [User])
users() {
return this.userService.findAll();
}
}
Armature (armature-graphql):
#[injectable]
#[derive(Clone)]
struct UserService { }
struct QueryRoot {
user_service: UserService,
}
#[Object]
impl QueryRoot {
async fn users(&self) -> Vec<User> {
self.user_service.find_all()
}
}
let schema = ProgrammaticSchemaBuilder::new()
.query(QueryRoot { user_service })
.mutation(EmptyMutation)
.subscription(EmptySubscription)
.build();
Dependency Injection with GraphQL
Inject Services into Resolvers
// Define your service
#[injectable]
#[derive(Clone)]
struct UserService {
database: DatabaseService,
}
// Method 1: Constructor injection (NestJS-style)
struct QueryRoot {
user_service: UserService,
}
#[Object]
impl QueryRoot {
async fn users(&self) -> Vec<User> {
self.user_service.get_all()
}
}
// Method 2: Context injection
// Add service to GraphQL context
let schema = ProgrammaticSchemaBuilder::new()
.query(query)
.add_service(user_service) // Available in context
.build();
// Use in resolver
#[Object]
impl QueryRoot {
async fn users(&self, ctx: &Context<'_>) -> Vec<User> {
let service = ctx.data::<UserService>().unwrap();
service.get_all()
}
}
Error Handling
Custom Errors
use armature_graphql::Error;
#[Object]
impl QueryRoot {
async fn user(&self, id: ID) -> Result<User> {
self.user_service
.get_by_id(&id)
.ok_or_else(|| Error::new("User not found"))
}
async fn validate_user(&self, email: String) -> Result<bool> {
if !email.contains('@') {
return Err(Error::new("Invalid email format"));
}
Ok(true)
}
}
Field Errors
#[Object]
impl User {
async fn sensitive_data(&self, ctx: &Context<'_>) -> Result<String> {
let auth = ctx.data::<AuthService>().unwrap();
if !auth.is_authorized(&self.id) {
return Err(Error::new("Unauthorized"));
}
Ok(self.sensitive_data.clone())
}
}
GraphQL Playground
Built-in Playground
Armature provides two playground options:
GraphiQL (Lightweight)
use armature_graphql::graphiql_html;
router.add_route(Route {
method: HttpMethod::GET,
path: "/playground".to_string(),
handler: Arc::new(move |_req| {
Box::pin(async move {
let html = graphiql_html("/graphql");
Ok(HttpResponse::ok()
.with_header("Content-Type".into(), "text/html".into())
.with_body(html.into_bytes()))
})
}),
});
GraphQL Playground
use armature_graphql::graphql_playground_html;
let html = graphql_playground_html("/graphql");
Best Practices
1. Use Input Objects for Complex Mutations
Good:
#[derive(InputObject)]
struct CreateUserInput {
name: String,
email: String,
role: Role,
}
async fn create_user(&self, input: CreateUserInput) -> User
Avoid:
async fn create_user(&self, name: String, email: String, role: Role) -> User
2. Implement Pagination
#[derive(SimpleObject)]
struct UserConnection {
edges: Vec<UserEdge>,
page_info: PageInfo,
}
#[derive(SimpleObject)]
struct UserEdge {
node: User,
cursor: String,
}
#[derive(SimpleObject)]
struct PageInfo {
has_next_page: bool,
has_previous_page: bool,
}
#[Object]
impl QueryRoot {
async fn users(&self, first: i32, after: Option<String>) -> UserConnection {
self.user_service.paginate(first, after)
}
}
3. Use DataLoader for N+1 Queries
use async_graphql::dataloader::*;
struct UserLoader {
user_service: UserService,
}
#[async_trait::async_trait]
impl Loader<ID> for UserLoader {
type Value = User;
type Error = Arc<Error>;
async fn load(&self, keys: &[ID]) -> Result<HashMap<ID, User>, Self::Error> {
Ok(self.user_service.get_by_ids(keys))
}
}
4. Add Field Descriptions
#[Object]
impl QueryRoot {
/// Get all users in the system
#[graphql(desc = "Retrieve a list of all users")]
async fn users(&self) -> Vec<User> {
self.user_service.get_all()
}
}
5. Use Guards for Authorization
use async_graphql::Guard;
struct RoleGuard {
role: Role,
}
#[async_trait::async_trait]
impl Guard for RoleGuard {
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
let user = ctx.data::<CurrentUser>()?;
if user.role == self.role {
Ok(())
} else {
Err("Unauthorized".into())
}
}
}
#[Object]
impl MutationRoot {
#[graphql(guard = "RoleGuard { role: Role::Admin }")]
async fn delete_user(&self, id: ID) -> bool {
self.user_service.delete(&id)
}
}
Testing
Unit Testing Resolvers
#[tokio::test]
async fn test_query_users() {
let service = UserService::default();
let query = QueryRoot { user_service: service };
let users = query.users().await;
assert!(!users.is_empty());
}
Integration Testing Schema
#[tokio::test]
async fn test_graphql_query() {
let schema = create_schema();
let query = r#"
query {
users {
id
name
}
}
"#;
let req = async_graphql::Request::new(query);
let res = schema.execute(req).await;
assert!(res.errors.is_empty());
assert!(res.data.is_object());
}
Performance Tips
- Use DataLoader - Batch database queries
- Limit Query Depth - Prevent deeply nested queries
- Add Query Complexity - Limit computational cost
- Cache Results - Use Redis or in-memory cache
- Optimize N+1 - Use DataLoader or JOIN queries
Common Patterns
Relay-Style Pagination
#[derive(SimpleObject)]
struct Connection<T> {
edges: Vec<Edge<T>>,
page_info: PageInfo,
}
Error Union Pattern
#[derive(Union)]
enum UserResult {
Success(User),
Error(UserError),
}
Batch Mutations
async fn batch_create_users(&self, inputs: Vec<CreateUserInput>) -> Vec<User>
Summary
Armature's GraphQL support provides:
โ Type-Safe - Compile-time schema validation โ DI Integration - Services injected into resolvers โ Full Featured - Queries, mutations, subscriptions โ Developer Friendly - Built-in playground โ Production Ready - Error handling, pagination, guards โ Performant - DataLoader, caching support
For more examples, see examples/graphql_api.rs in the repository.