Sharing my knowledge on web development and personal experiences

Dynamically include relations using API requests

Published at 2020-04-12 in English

When we are designing applications we mostly start by planning and considering our database tables schemas by trying to follow Database Normalization rules which defines that each table should contain its specific data only and any other related fields should be separated into other tables and then we can use relationships between these tables to get the complete integrated data.


But when we are designing and developing API Requests our viewpoint is different, here we care more about the response body data integrity and reading simplicity to the consumer developer, despite the way we designed our database, and also we have to care about improving the performance and speed. first by improving our code and following the performance tips, second by studying API Responses, how we can design and divide it to be meaningful, easy to use, and returns the right data which the consumer developer needs and expects from it.


Let’s start by taking as example, suppose we have an application that lists posts and their comments. And suppose we have two pages one for viewing the post details only, and another for viewing the post details wit  its comments list. What do you think is the best way for us to design our APIs requests to be high performance and easy to use.


First Solution:

We might first think about designing one simple API request to be responsible for viewing the post and its comments, the url can be (posts/{postId}) and the response like the following



{
    "data": {
        "title": "Fugit assumenda placeat corporis facere et ullam consequatur.",
        "body": "Necessitatibus iusto rerum harum et sed earum. Necessitatibus impedit est sunt aut reiciendis fuga. Quia aut necessitatibus autem occaecati magnam.",
        "photo_path": "https://lorempixel.com/640/480/cats/?18073",
        "published_at": "2007-06-05 08:07:11",
        "comments": [
            {
                "body": "Porro molestias maxime assumenda dolores.",
                "user": {
                    "name": "Delores Schmeler IV",
                    "email": "reilly.genevieve@example.net"
                }
            },
            {
                "body": "Animi asperiores rerum quas culpa cumque ab magnam.",
                "user": {
                    "name": "Murphy Legros",
                    "email": "emilie.weber@example.org"
                }
            }
        ]
    }
}


This solution is applicable and has nothing wrong, but it is preferable to be applied only when the associated relationship (in out case comments) records are not many, and we know that it won’t reach a certain maximum number of records so we can assure that performance will always be steady, but if we expect that the associated relationship records will add up until unknown number of records, in this situation this solutions is not the best because it force us to return all of the associated relationship records in this one request, therefore it will always return the comment on both pages, which on the post only page we won’t be viewing the comment, so this means we will wait for the request to process something we don’t need.


Second Solution: 

We can seperate the post details and the comments list on two seperate API requests, first one for the post details only (posts/{postId})




{
    "data": {
        "title": "Fugit assumenda placeat corporis facere et ullam consequatur.",
        "body": "Necessitatibus iusto rerum harum et sed earum. Necessitatibus impedit est sunt aut reiciendis fuga. Quia aut necessitatibus autem occaecati magnam.",
        "photo_path": "https://lorempixel.com/640/480/cats/?18073",
        "published_at": "2007-06-05 08:07:11"
    }
}



And the other one for the post comments list (/posts/{postId}/comments).


{
    "data": [
        {
            "body": "Porro molestias maxime assumenda dolores.",
            "user": {
                "name": "Delores Schmeler IV",
                "email": "reilly.genevieve@example.net"
            }
        },
        {
            "body": "Animi asperiores rerum quas culpa cumque ab magnam.",
            "user": {
                "name": "Murphy Legros",
                "email": "emilie.weber@example.org"
            }
        },
        {
            "body": "Aut odio qui odit est labore quod.",
            "user": {
                "name": "Daija Legros",
                "email": "hildegard61@example.net"
            }
        }
    ]
}




This way was can only load the things we want, on the post detail only page we can use the first API (posts/{postId}), and other page we can use both API (/posts/{postId}) and (posts/{postId}/comments) so we can view post and its comments, this is very good.

But did we make it easy to consume these API by developers? I don’t think so, because now they have to call two API requests to get the post with its comments, what if the post had more relations like tags, should we make another separate API request for it? This will make the total API request three.


This solution is good in terms of performance, but in terms of ease of use for consuming developers I can’t assure you. It  may be right if we knew that we will have a limited number of relationships on the Post, or we only want to view comments, so this solution depends on our system analysis.


Is there another solution?


Third Solution: 

How about returning only the relationships we want in the response and making it easy to use for consuming developers, we can make use of Eloquent Relationships Eager Loading on Laravel and which I suppose it can be found on other Frameworks too. This feature enables us to include the Resource relations (in our case Post) so we can get these relations (comments) when we request this Resource (post)


We can do this by defining the with array on the Post model


namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
   protected $with = ['comments'];

   public function comments()
   {
       return $this->hasMany(Comment::class);
   }
}


This way when we request the post we will get its comments with it, but didn’t we say we want the comments only when we need them?


Yes, by making use of eager loading feature, I made a way to only include the relations we want.


First: how can we define which relations we want to include it with the post?

We can define it by making use of API query parameters, by sending a parameter with the names of relations you want to include.


Second: how will we get these relations using this parameter?

We will add the requested relations name from the parameter to the with array on the post model.


By doing this, if we want the post details only we can request the API without sending the parameter, and when we need the comments we can send the parameter containing the comments relation name. So we can get it back on the response.


Since I needed this in many projects, I thought about making it into a package, Dynamic Relations Includes.

Let’s try to achieve what we need using this package


Installation:

We can install it using composer 

composer require kalshah/dynamic-relations-includes


Usage:

We will first need to use the IncludeRelations trait on Post Model 

use IncludeRelations;


Then we set which relations on the post model are allowed to be loaded using API query parameter.

protected $loadableRelations = ['comments'];


This array prevents us from loading relations that might hold sensitive data which we don’t want it to be automatically exposed, because we sometimes need to perform certain permissions before returning the relation records.


Finally the Post model will be like this:


namespace App;

use Illuminate\Database\Eloquent\Model;
use Kalshah\DynamicRelationsInclude\IncludeRelations;

class Post extends Model
{
    use IncludeRelations;

    protected $loadableRelations = ['comments'];

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}


Now we can request the post details only using (posts/{postId}) and response will be:



{
    "data": {
        "title": "Fugit assumenda placeat corporis facere et ullam consequatur.",
        "body": "Necessitatibus iusto rerum harum et sed earum. Necessitatibus impedit est sunt aut reiciendis fuga. Quia aut necessitatibus autem occaecati magnam.",
        "photo_path": "https://lorempixel.com/640/480/cats/?18073",
        "published_at": "2007-06-05 08:07:11"
    }
}


And we can include comments by sending (posts/{postID}?include=comments)


{
    "data": {
        "title": "Fugit assumenda placeat corporis facere et ullam consequatur.",
        "body": "Necessitatibus iusto rerum harum et sed earum. Necessitatibus impedit est sunt aut reiciendis fuga. Quia aut necessitatibus autem occaecati magnam.",
        "photo_path": "https://lorempixel.com/640/480/cats/?18073",
        "published_at": "2007-06-05 08:07:11",
        "comments": [
            {
                "body": "Porro molestias maxime assumenda dolores.",
                "user": {
                    "name": "Delores Schmeler IV",
                    "email": "reilly.genevieve@example.net"
                }
            },
            {
                "body": "Animi asperiores rerum quas culpa cumque ab magnam.",
                "user": {
                    "name": "Murphy Legros",
                    "email": "emilie.weber@example.org"
                }
            }
        ]
    }
}


In conclusion we were able to include relations based on the consumer developer request, this way our application is responsive to any relationships that need to be included.


You can read more about the package from docs, and also read about how to include relations records count, for example including comments of the post.



خولة الشح

Written by خولة الشح