Tutorial 5: Relationships and Hyperlinked APIs
Tutorial 5: Relationships and Hyperlinked APIs
Create APIs with relationships between resources and use hyperlinks for navigation.
URL Reverse Routing
Generate URLs for named routes using the router's reverse() method:
use reinhardt::prelude::*;
use std::collections::HashMap;
let router = DefaultRouter::new();
// Define route parameters
let mut params = HashMap::new();
params.insert("id".to_string(), "42".to_string());
// Generate URL from route name
let url = router.reverse("snippet-detail", ¶ms)?;
// url: "/snippets/42/"Hyperlinked Relations
Use hyperlinked fields in serializers to reference related resources:
use reinhardt::prelude::*;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnippetSerializer {
pub id: i64,
pub title: String,
pub code: String,
pub owner: String,
pub owner_url: String, // Hyperlink to owner resource
}
// Generate hyperlink
let owner_url = router.reverse("user-detail", &{
let mut params = HashMap::new();
params.insert("id".to_string(), owner_id.to_string());
params
})?;HyperlinkedModelSerializer
Create serializers with automatic hyperlink generation:
use reinhardt::prelude::*;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
id: i64,
username: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Snippet {
id: i64,
title: String,
code: String,
owner: User,
}
// Hyperlinked serializer representation
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnippetHyperlinked {
url: String, // Self URL
id: i64,
title: String,
code: String,
owner: String, // Owner URL
}Related Object Serialization
Include related objects in responses:
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct UserSerializer {
id: i64,
username: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnippetWithUser {
id: i64,
title: String,
code: String,
owner: UserSerializer, // Nested user object
}URL Patterns
Define URL patterns with route names:
use reinhardt::prelude::*;
let mut router = DefaultRouter::new();
// Register ViewSets with route names
let snippet_viewset = ModelViewSet::<Snippet, SnippetSerializer>::new("snippet");
let user_viewset = ModelViewSet::<User, UserSerializer>::new("user");
router.register_viewset("snippets", snippet_viewset);
router.register_viewset("users", user_viewset);
// Generated routes with names:
// snippet-list: GET/POST /snippets/
// snippet-detail: GET/PUT/PATCH/DELETE /snippets/{id}/
// user-list: GET/POST /users/
// user-detail: GET/PUT/PATCH/DELETE /users/{id}/Building Hyperlinked Responses
Create responses with hyperlinks:
use reinhardt::prelude::*;
use std::collections::HashMap;
async fn snippet_detail(
request: Request,
router: &DefaultRouter,
id: i64
) -> Result<Response> {
// Get snippet from database
let snippet = get_snippet(id).await?;
// Generate hyperlinks
let mut params = HashMap::new();
params.insert("id".to_string(), snippet.id.to_string());
let self_url = router.reverse("snippet-detail", ¶ms)?;
params.clear();
params.insert("id".to_string(), snippet.owner_id.to_string());
let owner_url = router.reverse("user-detail", ¶ms)?;
// Build response with hyperlinks
let response_data = SnippetHyperlinked {
url: self_url,
id: snippet.id,
title: snippet.title,
code: snippet.code,
owner: owner_url,
};
Ok(Response::ok(response_data))
}Many-to-Many Relations
Handle many-to-many relationships:
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Tag {
id: i64,
name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnippetWithTags {
id: i64,
title: String,
code: String,
tags: Vec<String>, // List of tag URLs
}
// Generate tag URLs
let tag_urls: Vec<String> = snippet.tags
.iter()
.map(|tag| {
let mut params = HashMap::new();
params.insert("id".to_string(), tag.id.to_string());
router.reverse("tag-detail", ¶ms).unwrap()
})
.collect();Complete Example
Full hyperlinked API implementation:
use reinhardt::prelude::*;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
id: i64,
username: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Snippet {
id: i64,
title: String,
code: String,
owner_id: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct UserSerializer {
url: String,
id: i64,
username: String,
snippets: Vec<String>, // URLs to user's snippets
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnippetSerializer {
url: String,
id: i64,
title: String,
code: String,
owner: String, // URL to owner
}
// In config/urls.rs
pub fn url_patterns() -> UnifiedRouter {
let mut router = DefaultRouter::new();
// Register ViewSets
let snippet_viewset = ModelViewSet::<Snippet, SnippetSerializer>::new("snippet");
let user_viewset = ModelViewSet::<User, UserSerializer>::new("user");
router.register_viewset("snippets", snippet_viewset);
router.register_viewset("users", user_viewset);
router.into()
}
// Example: Generate snippet URL via router.reverse()
// let mut params = HashMap::new();
// params.insert("id".to_string(), "1".to_string());
// let snippet_url = router.reverse("snippet-detail", ¶ms)?;
// => "/api/snippets/1/"
//
// params.insert("id".to_string(), "42".to_string());
// let user_url = router.reverse("user-detail", ¶ms)?;
// => "/api/users/42/"Benefits of Hyperlinked APIs
- Discoverability: Clients can navigate the API by following links
- Decoupling: URLs can change without breaking clients
- HATEOAS: Hypermedia as the Engine of Application State
- Self-documenting: Relationships are explicit in responses
Summary
In this tutorial, you learned:
- URL reverse routing with
router.reverse() - Creating hyperlinked relations between resources
- Using
HyperlinkedModelSerializer - Including related objects in responses
- Defining URL patterns with route names
- Building hyperlinked API responses
- Handling many-to-many relationships
- Benefits of hyperlinked APIs
Next tutorial: Tutorial 6: ViewSets and Routers