Introduction to the Component-based API

Leonardo Losoviz

If we're talking about APIs, we must talk about the current hot stuff...

GraphQL

GraphQL is (increasingly) popular

Advantages of GraphQL

  • It fetches exactly the required data, in a single request
query {
  featuredDirector {
    name
    country
    avatar
    films {
      title
      thumbnail
      actors {
        name
        avatar
      }
    }
  }
}
{
  data: {
    featuredDirector: {
      name: "George Lucas",
      country: "USA",
      avatar: "...",
      films: [
        { 
          title: "Star Wars: Episode I",
          thumbnail: "...",
          actors: [
            {
              name: "Ewan McGregor",
              avatar: "...",
            },
            {
              name: "Natalie Portman",
              avatar: "...",
            }
          ]
        },
        { 
          title: "Star Wars: Episode II",
          thumbnail: "...",
          actors: [
            {
              name: "Natalie Portman",
              avatar: "...",
            },
            {
              name: "Hayden Christensen",
              avatar: "...",
            }
          ]
        }
      ]
    }
  }
}

Name

Country

Avatar

Thumbnail

Title

Avatar

Name

Render <FeaturedDirector>:
  <div>
    <img src="{avatar}" class="pull-left">
    <h3>{name}</h3>
    <strong>Country:</strong> {country}
    {foreach films as film}
      <Film film={film} />
    {/foreach}
  </div>

Render <Film>:
  <div>
    <img src="{thumbnail}">
    <h4>{title}</h4>
    {foreach actors as actor}
      <Actor actor={actor} />
    {/foreach}
  </div>

Render <Actor>:
  <div>
    <img src="{avatar}">
    <h5>{name}</h5>
  </div>

Advantages of GraphQL

  • Suitable for coding with components
Render <FeaturedDirector>:
  <div>
    <img src="{avatar}" class="pull-left">
    <h3>{name}</h3>
    <strong>Country:</strong> {country}
    {foreach films as film}
      <Film film={film} />
    {/foreach}
  </div>

Render <Film>:
  <div>
    <img src="{thumbnail}">
    <h4>{title}</h4>
    {foreach actors as actor}
      <Actor actor={actor} />
    {/foreach}
  </div>
Render <FeaturedDirector>:
  <div>
    <img src="{avatar}" class="pull-left">
    <h3>{name}</h3>
    <strong>Country:</strong> {country}
    {foreach films as film}
      <Film film={film} />
    {/foreach}
  </div>
query {
  featuredDirector {
    name
    country
    avatar
    films {
      title
      thumbnail
      actors {
        name
        avatar
      }
    }
  }
}

So far so good...

But GraphQL is not perfect 😥

(Some) Issues with GraphQL

  1. Not cacheable on the back-end
    • Can be cached on client-side, but it adds complexity to the app
  2. Susceptible to DoS attacks
    • Can be mitigated, but it adds complexity to the app

Let me present you a project I've been working on...

The

Component-based API

  • It's a work in progress, to be released in a few months
  • All fundamentals have been implemented
  • Open specification (following GraphQL)
  • It attempts to combine the best from REST and GraphQL

Fetching data

  • It fetches exactly what data is required, like GraphQL
  • But the query is done through the URL, like REST
  • This solves the cacheability issue
query {
  featuredDirector {
    name
    country
    avatar
    films {
      title
      thumbnail
      actors {
        name
        avatar
      }
    }
  }
}
GET 
    /featured-director/?fields=name|country|avatar
GET 
    /featured-director/?fields=name|country|avatar,
    films.title|thumbnail
GET 
    /featured-director/?fields=name|country|avatar,
    films.title|thumbnail,films.actors.name|avatar
query {
  featuredDirector {
    name
    country
    avatar
    







  }
}
query {
  featuredDirector {
    name
    country
    avatar
    films {
      title
      thumbnail




    }
  }
}
query {
  featuredDirector {
    name
    country
    avatar
    films {
      title
      thumbnail
      actors {
        name
        avatar
      }
    }
  }
}

Shape of the fetched data

  • Instead of mirroring the schema (entities containing other entities, as in GraphQL), it mirrors the relationships among entities as they have been stored on a relational DB
  • This solves the issue with DoS attacks
{
  data: {
    featuredDirector: {
      name: "George Lucas",
      country: "United States",
      avatar: "...",
      films: [
        { 
          title: "Star Wars: Episode I",
          thumbnail: "...",
          actors: [
            {
              name: "Ewan McGregor",
              avatar: "...",
            },
            {
              name: "Natalie Portman",
              avatar: "...",
            }
          ]
        },
        { 
          title: "Star Wars: Episode II",
          thumbnail: "...",
          actors: [
            {
              name: "Natalie Portman",
              avatar: "...",
            },
            {
              name: "Hayden Christensen",
              avatar: "...",
            }
          ]
        }
      ]
    }
  }
}
{
  databases: {
    primary: {
      people {
        1: {
          name: "George Lucas",
          country: "United States",
          avatar: "...",
          films: [1, 2]
        },
        2: {
          name: "Ewan McGregor",
          avatar: "..."
        },
        3: {
          name: "Natalie Portman",
          avatar: "..."
        },
        4: {
          name: "Hayden Christensen",
          avatar: "..."
        }
      },
      films: {
        1: { 
          title: "Star Wars: Episode I",
          thumbnail: "...",
          actors: [2, 3]
        },
        2: { 
          title: "Star Wars: Episode II",
          thumbnail: "...",
          actors: [3, 4]
        }
      }
    }
  }
}
query {
  posts {
    title
    author {
      name
      followers {
        name
        recommendedPosts {
          title
          author {
            name
          }
        }
      }
      recommendedPosts {
        title
        author {
          name
        }
      }
    }
  }
}
{
  databases: {
    primary: {
      posts {
        1: {
          title: "My first post",
          author: 1
        },
        2: {
          title: "Some other post",
          author: 1
        },
        3: {
          title: "Yet another post",
          author: 3
        },
      },
      users: {
        1: { 
          name: "Leo",
          followers: [2, 3],
          recommendedPosts: [2, 3]
        },
        2: { 
          name: "Julia",
          followers: [1],
          recommendedPosts: [1, 2, 3]
        },
        3: {...}
      }
    }
  }
}

Shape of the fetched data

API response

{
  data: {
    "featured-director": {
      dbobjectids: [1]
    }
  },
  settings: {
    "featured-director": {
      dbkeys: {
        id: "people",
        films: "films",
        films.actors: "people"
      }
    }
  },
  databases: {
    primary: {
      people {
        1: {
          name: "George Lucas",
          country: "United States",
          avatar: "...",
          films: [1, 2]
        },
        2: {
          name: "Ewan McGregor",
          avatar: "..."
        },
        3: {
          name: "Natalie Portman",
          avatar: "..."
        },
        4: {
          name: "Hayden Christensen",
          avatar: "..."
        }
      },
      films: {
        1: { 
          title: "Star Wars: Episode I",
          thumbnail: "...",
          actors: [2, 3]
        },
        2: { 
          title: "Star Wars: Episode II",
          thumbnail: "...",
          actors: [3, 4]
        }
      }
    }
  }
}
  • Data is normalized
  • A unique DB contains all data in client ⇒ Client cache
  • When a component fetches its data, all other existing components in the page can be re-rendered
  • Link 1 | Link 2
GET 
    /featured-director/?fields=name|country|avatar

featured-director

GET 
    /featured-director/?fields=name|country|avatar,
    films.title|thumbnail
GET 
    /featured-director/?fields=name|country|avatar,
    films.title|thumbnail,films.actors.name|avatar

Let's see how it works

Component Architecture

  • Components are implemented partly in the front-end, partly in the back-end
  • Progressively enhanced from API to App in 4 Layers:
  1. Data layer ⇒ API
  2. Configuration layer ⇒ App structure
  3. View layer ⇒ App
  4. Reactivity ⇒ Dynamic App
  • The App can be modeled as an extension of the API
  • Querying specific fields through the API is simply a use case of the component-based architecture

(Back-end)

(Front-end)

Component Architecture

  • Component hierarchy, props, data fields, configuration (PHP)
<div class="dropdown {{classes.dropdown}}">
  <button class="dropdown-toggle {{classes.btn}}">
    {{{text}}}
  </button>
  <ul class="dropdown-menu" role="menu">
    {{#each submodules}}
      <li role="presentation">
        {{#withModule ../. this}}
          {{enterModule ../../.}}
        {{/withModule}}
      </li>
    {{/each}}
  </ul>
</div>
class Components extends AbstractComponents
{
  public function getSubmodules($module)
  {
    ...
  }
  public function initProps($module, &$props)
  {
    ...
  }
  public function getDataFields($module, $props)
  {
    ...
  }
  public function getConfiguration($module, $props)
  {
    ...
  }
}

Back-end

Front-end

  • Every component creates/receives its own context

  • The view doesn't know or care about its subcomponents

  • Props can be set vertically and horizontally

  • View (Handlebars)

Component Architecture

GET /posts/lovely-tango/

post

post-title

post-thumbnail

post-content

"post"
  submodules
    "post-title"
    "post-thumbnail"
    "post-content"
class LayoutComponents extends AbstractComponents
{
  public const MODULE_POST = 'post';
  public const MODULE_POSTTITLE = 'post-title';
  public const MODULE_POSTTHUMBNAIL = 'post-thumbnail';
  public const MODULE_POSTCONTENT = 'post-content';

  public function getSubmodules($module)
  {
    switch ($module) {
      case self::MODULE_POST:
        return [
          self::MODULE_POSTTITLE,
          self::MODULE_POSTTHUMBNAIL,
          self::MODULE_POSTCONTENT,
        ];
    }

    return [];
  }
}

Component hierarchy

  • For retrieving data, each component must define what data fields it needs from the DB just for itself (i.e. without including the data fields for its subcomponents)
  • The endpoint URL from which to fetch the data, and also the query to execute against the DB, can be automatically generated from the component hierarchy itself

Component Architecture

GET /posts/lovely-tango/

post

post-title

post-thumbnail

post-content

SELECT 
  title, thumbnail, content 
FROM 
  posts 
WHERE
  id = 37

"title"

"thumbnail"

"content"

/posts/lovely-tango/?fields=title|
thumbnail|content

Set domain to current post

Endpoint URL

DB Query

Component Architecture

GET /posts/lovely-tango/

post

post-author

SELECT 
  p.title, p.content, p.author, 
  u.name, u.avatar 
FROM 
  posts p 
INNER JOIN 
  users u 
WHERE 
  p.id = 37 AND p.author = u.id

"avatar"

/posts/lovely-tango/?fields=title|
thumbnail|content,author.name|avatar

Set domain to current post.author

user-avatar

user-name

"name"

Set domain to current post

Endpoint URL

DB Query

Component Architecture

Component Architecture

GET /posts/lovely-tango/
<div class="{{class}}">
  {{#each submodules}}
    {{#withModule ../. this}}
      {{enterModule ../../. 
        dbObjectKey=../../dbkeys.id 
        dbObjectID=../../dbObjectIDs
      }}
    {{/withModule}}
  {{/each}}
</div>

View (Handlebars)

class LayoutComponents extends AbstractComponents
{
  public const MODULE_POST = 'post';
  public const MODULE_POSTTITLE = 'post-title';
  public const MODULE_POSTAUTHOR = 'post-author';

  public function getSubmodules($module)
  {
    switch ($module) {
      case self::MODULE_POST:
        return [
          self::MODULE_POSTTITLE,
          self::MODULE_POSTAUTHOR,
        ];
    }

    return [];
  }
}

Configuration (PHP)

post

post-title

post-author

GET /posts/lovely-tango/
{{#with dbObject}}
  <{{../header}} class="{{../class}}">
    {{{title}}}
  </{{../header}}>
{{/with}}

View (Handlebars)

class LayoutComponents extends AbstractComponents
{
  public function getDataFields($module, $props)
  {
    switch ($module) {
      case self::MODULE_POSTTITLE:
        return [
          'title',
        ];
    }

    return [];
  }

  public function getConfiguration($module, $props)
  {
    switch ($module) {
      case self::MODULE_POSTTITLE:
        return [
          'header' => 'h1',
          'class' => 'main-title',
        ];
    }

    return [];
  }
}

Configuration (PHP)

post-title

Component Architecture

{{#with dbObject}}
  <h1>
    {{{title}}}
  </h1>
{{/with}}
class LayoutComponents extends AbstractComponents
{
  public function getDataFields($module, $props)
  {
    switch ($module) {
      case self::MODULE_POSTTITLE:
        return [
          'title',
        ];
    }

    return [];
  }
}

Component Architecture

GET /posts/lovely-tango/
<div class="{{class}}">
  {{#each submodules}}
    {{#withModule ../. this}}
      {{enterModule ../../. 
        dbObjectKey=../../dbkeys.author 
        dbObjectID=../../dbObject.author
      }}
    {{/withModule}}
  {{/each}}
</div>

View (Handlebars)

class LayoutComponents extends AbstractComponents
{
  public const MODULE_POSTAUTHOR = 'post-author';
  public const MODULE_AUTHORNAME = 'author-name';
  public const MODULE_AUTHORAVATAR = 'author-avatar';

  public function getSubmodules($module)
  {
    switch ($module) {
      case self::MODULE_POSTAUTHOR:
        return [
          self::MODULE_AUTHORNAME,
          self::MODULE_AUTHORAVATAR,
        ];
    }

    return [];
  }
}

Configuration (PHP)

post-author

Isomorphism

  • The application can run on the server or the client!
    ✅ Handlebars templates can be compiled to PHP (Server)
    ✅ PHP Configuration can be exported as a .json file (Client)
[
  "top-module" => [
    "submodules" => [
      "module-level1" => [
        "submodules" => [
          "module-level11" => [
            "submodules" => [...]
          ],
          "module-level12" => [
            "submodules" => [
              "module-level121" => [
                "submodules" => [...]
              ]
            ]
          ]
        ]
      ]
    ]
  ]
]
{
  "top-module": {
    submodules: {
      "module-level1": {
        submodules: {
          "module-level11": {
            ...
          },
          "module-level12": {
            submodules: {
              "module-level121": {
                ...
              }
            }
          }
        }
      }
    }
  }
}

PHP

JSON

  • Server-Side Rendering / Serverless application

Target Specific Components

GET /posts/a-lovely-tango/?modulepaths[]=post.post-title&modulepaths[]=post.post-content
  • The component is its own API
  • The website is its own API
  • Easy to implement Single-Page Applications
  • Easy to create pattern libraries

post

post-title

post-content

GET /posts/a-lovely-tango/?modulepaths[]=post.post-title
GET /posts/a-lovely-tango/

COPE

(Create Once, Publish everywhere)

  • Single source of truth code for multiple platforms: web, email, iOS/Android apps...
class HTMLCSSLayoutComponents extends AbstractComponents
{
  
}
class JSLayoutComponents extends HTMLCSSLayoutComponents
{
  
}

In a nutshell:

Advantages of this Architecture

  • Client-side cache
  • Easy to change the behavior/appearance of the application
  • Isomorphism: Server-Side Rendering/Serverless applications
  • The component (and the website) is its own API
  • Easy to build Single-Page Applications
  • Easy to generate a pattern library
  • Can implement COPE (Create Once, Publish everywhere)
  • Reduced complexity compared to other stacks
  • More output in less time, with fewer resources

Status of the Project

  • MIT license
  • Not ready yet, still several months away
  • (Can't wait? Contributors are welcome 😝)

Thanks!

Leonardo Losoviz

👋