Build a Server Driven UI
In a website or app with dynamic content, having a UI that is controlled by a backend service makes it very easy to manage, scale and update. Let's take a look at how we can achieve this.
Published on April 20th, 2023
Advantage of having a Server driven UI
Suppose you want to build an app or website with a backend data source. The high-level data structure will be fairly straightforward, consisting of the following components:
A Data Source: This can be a database or a third-party API service
A Backend API service to access the data (REST, GraphQL)
A Frontend which fetches the data and displays a UI to the user, which can be an app or a website
Let's take this blog you are reading as an example.
With the above logic, there should be a backend service exposing REST APIs, which I will call from this webpage (which is built using React, by the way). When the API is called, my backend will then accesses my database where the blog content is stored (MongoDB in this case). It will retrieve the data and send it back in JSON format, which will look something like this:
{
"heading": "Build a Backend driven UI",
"description": "In a website or app with...",
"date": "...",
"content": "Advantage of having..."
}
I can make adjustments to make it easier, such as storing the content in multiple fields like headings, code blocks, etc., which will ultimately make it more complex. Additionally, the frontend will have to perform a lot of extra computation to process the data and render the UI.
This approach also results in a Tightly Coupled Design, requiring significant effort later when I want to make even minor changes, making it not scalable. Let's consider a situation where my Code Block component is causing a crash; in that case, until the bug is fixed, I have only two options:
Remove the Code Block component manually by removing it from the frontend. (This is even more difficult for apps because you will need to push an update).
Add code in the REST API to not send the Image data and hope the frontend handles it.
Not very elegant. So let's look at what we can do.
The above case is very simplified, and there will be other fallback solutions for more robust and large-scale websites, such as feature gates. However, for this blog, which is a hobby project, the scope and time are not as extensive.
The Solution
Let's consider a different approach, where instead of my frontend determining what and where to render based on raw data, a backend middleware service sends direct instructions to the frontend on what to do.
If we send precomputed data, something like this, it will make it easier for the frontend to render
{
"pageId": "random_id",
"pageComponents": [
{
"id": "random_id",
"widgetType": "BLOG_PAGE_HEADING",
"data": {
"text": "Build a Backend Driven UI"
}
},
{
"id": "random_id",
"widgetType": "BLOG_PAGE_DESCRIPTION",
"data": {
"text": "In a website or app with dynamic content..."
}
},
{
"id": "random_id",
"widgetType": "BLOG_CONTENT_HEADING",
"data": {
"text": "Advantage of having a Backend driven UI"
}
}
//More widgets continue
]
}
Since we receive parsed data already, there is no additional effort for the frontend to perform any computation. We just need to pass the respective data to their respective widgets.
Let's break it down step by step on how we can achieve this:
When this blog page is requested, I will make an API call to my middleware, which will be something like this: middleware.url/getBlogPage/blog_id
The middleware will get the blog_id and then make an API call to my backend, which will be something like this: backend.url/getBlogData/blog_id
Once the middleware receives the blog data, it's now time to parse the raw data and create a JSON data object as described earlier.
The blog content on this page is actually stored in Markdown format in my database. I then iterate over the content and create the component data object, which is done in my middleware. To find out how that's achieved, you can read my more detailed blog about it here: Convert Markdown content to UI components
On the frontend, we then build individual widgets that will accept the data received from the middleware. (Make sure that the contracts between the middleware and frontend are predefined and followed, or else there will be a discrepancy in the data sent and received.) For the BLOG_PAGE_HEADING, we will create a React widget something like this:
export interface BlogPageHeadingData {
heading: string;
}
const BlogPageHeading: React.FC<BlogPageHeadingData> = ({ data }) => {
return (
<div>
<h1 className="BlogPageHeadingText">{data.text}</h1>
</div>
);
};
export default BlogPageHeading;
We now need a Widget Renderer that can return the widget to be rendered based on the widgetType we receive:
export type WidgetDataTypes =
| BlogPageHeadingWidget
| BlogPageDescriptionWidget
| BlogPageTimeWidget
| BlogContentHeadingWidget
//Add more of your Widgets here
export const getWidgetForType = (widget: WidgetDataTypes, data: any) => {
switch (widget) {
case 'BLOG_PAGE_HEADING':
return <BlogPageHeading {...(data as BlogPageHeadingData)} />;
case 'BLOG_PAGE_DESCRIPTION':
return <BlogPageDescription {...(widget as BlogPageDescriptionData)} />;
case 'BLOG_CONTENT_HEADING':
return <BlogContentHeading {...(widget as BlogPageContentData)} />;
}
}
Let's connect it all together on the Blog Page screen where the API call takes place:
export interface BlogScreenData {
components: WidgetDataTypes[];
}
const BlogScreen = () => {
//URL id of the page
const { urlId } = useParams();
const [screenData, setScreenData] =
useState <
BlogScreenData >
{
components: [],
};
const fetchBlogScreenData = useCallback(() => {
if (urlId) {
//An Axios Api call that fetches the data
getBlogScreenData(urlId).then((blogScreenData: BlogScreenData | null) => {
if (data) {
setScreenData(blogScreenData);
}
});
}
}, []);
useEffect(() => {
fetchBlogScreenData();
}, [fetchBlogScreenData]);
const renderscreenData = () => {
const widgets = screenData.components.map((widget) => {
return (
<div key={widget.id}>
{(getWidgetForType(widget.widgetType), widget.data)}
</div>
);
});
return widgets;
};
return (
<div>
<article>{renderscreenData()}</article>
</div>
);
};
As you can see above, rendering the page is now completely driven by the API response we get from the middleware. All our frontend does is:
Receive the computed data from the middleware, which is an array of widget data
Iterate over the data in the Blog Page screen
Based on the widget data, we then get the respective widget and render it on the screen
Now, if I have to stop a widget from rendering or change the order in which the page components are rendered, I just have to ensure the response from the middleware changes accordingly, requiring no changes on the frontend.
Additionally, if I want to add an existing widget to any of my pages, I just have to add the widget data to the respective page's response data.
Further Improvements
We now have the basics sorted out, but there are some necessary and optional features we can add to make managing the content even easier and more dynamic.
In the above case, if I have to remove a widget, I still have to make changes to the middleware. To avoid that, we can have a config service that is responsible for informing the middleware what to render and in what order. It can be a simple JSON object, as shown below:
{
"blogPage": [
"BLOG_PAGE_HEADING",
"BLOG_PAGE_DESCRIPTION",
"BLOG_PAGE_TIME",
"BLOG_CONTENT_HEADING"
//More widgets
],
"homePage": []
//More Pages
}
If tomorrow I want to launch a newly designed Blog page, but only roll it out to 10% of users to see how it performs or to keep crashes to a minimum, I can have an A/B service that fetches the new or default page config accordingly.
Technically, you can add a lot of dynamic data to your widgets, like font color, background color, etc., but finding a balance is necessary. Overdoing the backend-driven aspect can sometimes lead to an overall increase in effort for development and updates. For example, in my case, I have a theme where all headings will always be green. It's unnecessary to send font color in the widget data, and if someday I want to change the color of all my headings, I just have to make a single change on my frontend which is easier to do
Conclusion
Designing an app or website where at least some aspects of the UI are server-driven makes things much easier to maintain and scale. The initial effort may be slightly higher, and avoiding over-engineering is not an easy task either. However, in my personal experience building TechItUp with this technique, even though it may not have been strictly necessary, made the development process easier, and now adding new blogs is a cakewalk as well.
Sign Up 🚀
A Newsletter for
Develope
â–Ž
This Blog is intended to create content for people who are interested in Tech and everthing around it. My newsletter is designed to expand on tha same! I will notify you where there is new content so that you never miss out!
No Spam. Unsubscribe Anytime.