Can you explain why? This advice doesn't make intuitive sense for me.
For example, you may want to keep some fields internal and not expose them to users, or you may want to normalize your DB schema so some fields are stored in a linked table, or maybe you had something as timestamp+duration, and need to keep the same external API for compatibility, but also want to refactor it internally into two timestamps.
#[derive(Serialize, Deserialize, Identifiable, Queryable)]
#[serde(rename_all = "camelCase")]
struct User {
pub id: Uuid,
pub username: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, Identifiable, Queryable, Associations)]
#[belongs_to(User)]
#[serde(rename_all = "camelCase")]
struct Business {
pub id: Uuid,
pub name: String,
pub user_id
}
And their respective endpoints: #[get("/users)]
fn user_index(connection: &PgConnection) -> Result<Json<Vec<User>>, Status> {
// Going to pretend like we have some helper methods to help us out here
User::read_all().get_results::<User>(&*connection).map_err(|_| Status::Unavailable)?
}
#[get("/companies")]
fn company_index(connection: &PgConnection) -> Result<Json<Vec<User>>, Status> {
// Going to pretend like we have some helper methods to help us out here
Company::read_all().get_results::<Company>(&*connection).map_err(|_| Status::Unavailable)?
}
This is all well and good until maybe you have a UserPreferences model, which may or may not need its own route. Unlike other languages, you can't easily add attributes to structs. So if you made the choice to embed your UserPreferences into your User api response, you'd have to either: craft a json object from the json! macro, or alter all of your models anyways to introduce the new structure. Now your project/response structure would be: #[derive(Serialize, Deserialize)]
struct UserApiResponse {
id: Uuid,
username: String.
preferences: Option<UserPreferencesApiResponse>,
company_id: Option<Uuid>
}
impl From<User> for UserApiResponse {
fn from(user: User) -> Self {
Self {
id: user.id,
username: user.username,
preferences: None,
company_id: None,
}
}
}
impl UserApiResponse {
pub fn with_preferences<P: Into<UserApiPreferences>>(&mut self, preferences: P) {
self.preferences = Some(preferences.into());
}
pub fn with_company<C: Into<CompanyApiResponse>>(&mut self, company: C) {
self.company_id = company.id;
}
}
#[get("/users)]
fn user_index(connection: &PgConnection) -> Result<Json<Vec<User>>, Status> {
let preferences = Preferences::read_all().get_results(&*connection).map_err(|_| Status::Unavailable)?;
User::read_all().get_results::<User>(&*connection).map(|user| {
let mut user: UserApiResponse = user.into();
user.with_preferences(preferences.first().unwrap());
}).map_err(|_| Status::Unavailable)?
}
The code doesn't work out of the box but I hope that it conveys the idea that I'm trying to get across. This is sort of the same idea as Marshmallow in python or ActiveModelSerializers for Rails, but the problem is more pointed in rust because at least in python/ruby you can just shove on values as you need them (debatable if this is a good thing). The other thing that we've gained is consistency. Any change to an underlying serialization model is automatically reflected in any endpoint that may utilize it (also debatable if this is a good thing).To really drive the point home, consider what would need to happen if we serialized out the database models directly from a REST standpoint. The steps would be:
1. Fetch the user
2. Fetch the preferences by filtering on user id (requires a query parameter)
3. Fetch the company by filtering on user id (requires a query parameter)
In this new approach, we cut only one response, but if we were using GraphQL/JsonAPI we can cut this down even more.If we stuck with our base schema we'd be locked into always modifying our database schema models, which IMO ties too much to the data access layer, or having separate fetches per request which is non ideal for a 250ms response time per request. In this world, we can go a step further without ever altering our DB models, which allows us to reason about them in a more dumb/CRUD way.