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
}
}
}
}
Name
Country
Avatar
Thumbnail
Title
Avatar
Name
Director
name: George Lucas
country: USA
avatar: george-lucas.jpg
Film
title: The Phantom Menace
thumbnail: episode-1.jpg
title: Attack of the Clones
thumbnail: episode-2.jpg
Film
films
Actor
name: Nathalie Portman
avatar: portman.jpg
Actor
name: Ewan McGregor
avatar: mcgregor.jpg
actors
Actor
name: Hayden Christensen
avatar: christensen.jpg
Actor
actors
name: Nathalie Portman
avatar: portman.jpg
Director
name: George Lucas
country: USA
avatar: george-lucas.jpg
Film
title: The Phantom Menace
thumbnail: episode-1.jpg
title: Attack of the Clones
thumbnail: episode-2.jpg
Film
films
Actor
name: Nathalie Portman
avatar: portman.jpg
Actor
name: Ewan McGregor
avatar: mcgregor.jpg
actors
Actor
name: Hayden Christensen
avatar: christensen.jpg
Actor
actors
name: Nathalie Portman
avatar: portman.jpg
Director
Film
Actor
Director
name: George Lucas
country: USA
avatar: george-lucas.jpg
Film
title: The Phantom Menace
thumbnail: episode-1.jpg
title: Attack of the Clones
thumbnail: episode-2.jpg
filmType: Film
name: Nathalie Portman
avatar: portman.jpg
Actor
name: Ewan McGregor
avatar: mcgregor.jpg
name: Hayden Christensen
avatar: christensen.jpg
actorType: Actor
films: [3, 8]
actors: [4, 6]
actors: [6, 7]
ID: 3
ID: 8
ID: 4
ID: 6
ID: 7
ID: 2
Director
name: George Lucas
country: USA
avatar: george-lucas.jpg
Film
title: The Phantom Menace
thumbnail: episode-1.jpg
title: Attack of the Clones
thumbnail: episode-2.jpg
filmType: Film
name: Nathalie Portman
avatar: portman.jpg
Actor
name: Ewan McGregor
avatar: mcgregor.jpg
name: Hayden Christensen
avatar: christensen.jpg
actorType: Actor
films: [3, 8]
actors: [4, 6]
actors: [6, 7]
ID: 3
ID: 8
ID: 4
ID: 6
ID: 7
ID: 2
preferredActors: [4, 9]
name: Leonardo DiCaprio
avatar: dicaprio.jpg
ID: 9
Director
name: George Lucas
country: USA
avatar: george-lucas.jpg
Film
title: The Phantom Menace
thumbnail: episode-1.jpg
title: Attack of the Clones
thumbnail: episode-2.jpg
filmType: Film
name: Nathalie Portman
avatar: portman.jpg
Actor
name: Ewan McGregor
avatar: mcgregor.jpg
name: Hayden Christensen
avatar: christensen.jpg
actorType: Actor
films: [3, 8]
actors: [4, 6]
actors: [6, 7]
ID: 3
ID: 8
ID: 4
ID: 6
ID: 7
ID: 2
preferredActors: [4, 9]
name: Leonardo DiCaprio
avatar: dicaprio.jpg
ID: 9
Director
name: James Cameron
ID: 3
direcorType: Director
preferredDirector: 3
preferredDirector: 2
preferredDirector: 5
name: Steven Spielberg
ID: 5
Director
Film
Actor
Director
So far so good...
But GraphQL is not perfect 😥
(Some) Issues with GraphQL
- Not cacheable on the back-end
- Can be cached on client-side, but it adds complexity to the app
- 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]
}
}
}
}
}
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:
- Data layer ⇒ API
- Configuration layer ⇒ App structure
- View layer ⇒ App
- 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