JSX Over The Wire
Turning your API inside-out.
JSX Over The Wire
April 16, 2025
Suppose you have an API route that returns some data as JSON:
app.get('/api/likes/:postId', async (req, res) => {
const postId = req.params.postId;
const [post, friendLikes] = await Promise.all([
getPost(postId),
getFriendLikes(postId, { limit: 2 }),
]);
const json = {
totalLikeCount: post.totalLikeCount,
isLikedByUser: post.isLikedByUser,
friendLikes: friendLikes,
res.json(json);
});
You also have a React component that needs that data:
function LikeButton({
totalLikeCount,
isLikedByUser,
friendLikes
}) {
let buttonText = 'Like';
if (totalLikeCount > 0) {
// e.g. "Liked by You, Alice, and 13 others"
buttonText = formatLikeText(totalLikeCount, isLikedByUser, friendLikes);
return (
<button className={isLikedByUser ? 'liked' : ''}>
{buttonText}
</button>
How do you get that data into that component?
You could pass it from a parent component using some data fetching library:
function PostLikeButton({ postId }) {
const [json, isLoading] = useData(`/api/likes/${postId}`);
// ...
return (
<LikeButton
totalLikeCount={json.totalLikeCount}
isLikedByUser={json.isLikedByUser}
friendLikes={json.friendLikes}
That’s one way of thinking about it.
But have another look at your API:
app.get('/api/likes/:postId', async (req, res) => {
const postId = req.params.postId;
const [post, friendLikes] = await Promise.all([
getPost(postId),
getFriendLikes(postId, { limit: 2 }),
]);
const json = {
totalLikeCount: post.totalLikeCount,
isLikedByUser: post.isLikedByUser,
friendLikes: friendLikes,
res.json(json);
});
Do these lines remind you of anything?
Props. You’re passing props. You just didn’t specify where to.
But you already know their final destination—LikeButton.
Why not just fill that in?
app.get('/api/likes/:postId', async (req, res) => {
const postId = req.params.postId;
const [post, friendLikes] = await Promise.all([
getPost(postId),
getFriendLikes(postId, { limit: 2 }),
]);
const json = (
<LikeButton
totalLikeCount={post.totalLikeCount}
isLikedByUser={post.isLikedByUser}
friendLikes={friendLikes}
res.json(json);
});
Now the “parent component” of LikeButton is the API itself.
Wait, what?
Weird, I know. We’re going to worry about whether it’s a good idea later. But for now, notice how this inverts the relationship between components and the API. This is sometimes known as the Hollywood Principle: “Don’t call me, I’ll call you.”
Your components don’t call your API.
Instead, your API returns your components.
Why would you ever want to do that?
- Part 1: JSON as Components
- Part 2: Components as JSON
- Part 3: JSX Over The Wire
Part 1: JSON as Components
Model, View, ViewModel
There is a fundamental tension between how we want to store information and how we want to display it. We generally want to store more things than we display.
For example, consider a Like button on a Post. When we store Likes for a given Post, we might want to represent them as a table of Like rows like this:
type Like = {
createdAt: string, // Timestamp
likedById: number, // User ID
postId: number // Post ID
Let’s call this kind of data a “Model”. It represents the raw shape of the data.
type Model = Like;
So our Likes database table might contain data of that shape:
createdAt: '2025-04-13T02:04:41.668Z',
likedById: 123,
postId: 1001
}, {
createdAt: '2025-04-13T02:04:42.668Z',
likedById: 456,
postId: 1001
}, {
createdAt: '2025-04-13T02:04:43.668Z',
likedById: 789,
postId: 1002
}, /* ... */]
However, what we want to display to the user is different.
What we want to display is the number of Likes for that Post, whether the user has already liked it, and the names of their friends who also liked it. For example, the Like button could appear pressed in (which means that you already liked this post) and say “You, Alice, and 13 others liked this.” Or “Alice, Bob, and 12 others liked this.”
type LikeButtonProps = {
totalLikeCount: number,
isLikedByUser: boolean,
friendLikes: string[]
Let’s call this kind of data a “ViewModel”.
type ViewModel = LikeButtonProps;
A ViewModel represents data in a way that is directly consumable by the UI (i.e the view). It is often significantly different from the raw Model. In our example:
- ViewModel’s
totalLikeCountis aggregated from individualLikemodels.
- ViewModel’s
isLikedByUseris personalized and depends on the user.
- ViewModel’s
friendLikesis both aggregated and personalized. To calculate it, you’d have to takes the Likes for this post, filter them down to likes from friends, and get the first few friends’ names (which are likely stored in a different table).
Clearly, Models will need to turn into ViewModels at some point. The question is where and when this happens in the code, and how that code evolves over time.
REST and JSON API
The most common way to solve this problem is to expose some kind of a JSON API that the client can hit to assemble the ViewModel. There are different ways to design such an API, but the most common way is what’s usually known as REST.
The typical way to approach REST (let’s say we’ve never read this article) is to pick some “Resources”—such as a Post, or a Like—and provide JSON API endpoints that list, create, update, and delete such Resources. Naturally, REST does not specify anything about how you should shape these Resources so there’s a lot of flexibility.
Often, you might start by returning the shape of the Model:
// GET /api/post/123
title: 'My Post',
content: 'Hello world...',
authorId: 123,
createdAt: '2025-04-13T02:04:40.668Z'
So far so good. But how would you incorporate Likes into this? Maybe totalLikeCount and isLikedByUser could be a part of the Post Resource:
// GET /api/post/123
title: 'My Post',
content: 'Hello world...',
authorId: 123,
createdAt: '2025-04-13T02:04:40.668Z',
totalLikeCount: 13,
isLikedByUser: true
Now, should friendLikes also go there? We need this information on the client.
// GET /api/post/123
title: 'My Post',
authorId: 123,
content: 'Hello world...',
createdAt: '2025-04-13T02:04:40.668Z',
totalLikeCount: 13,
isLikedByUser: true,
friendLikes: ['Alice', 'Bob']
Or are we starting to abuse the notion of a Post by adding too much stuff to it? Okay, how about this, maybe we could offer a separate endpoint for a Post’s Likes:
// GET /api/post/123/likes
totalCount: 13,
likes: [{
createdAt: '2025-04-13T02:04:41.668Z',
likedById: 123,
}, {
createdAt: '2025-04-13T02:04:42.668Z',
likedById: 768,
}, /* ... */]
So a Post’s Like becomes its own “Resource”.
That’s nice in theory but we’re going to need to know the likers’ names, and we don’t want to make a request for each Like. So we need to “expand” the users here:
// GET /api/post/123/likes
totalCount: 13,
likes: [{
createdAt: '2025-04-13T02:04:41.668Z',
likedBy: {
id: 123,
firstName: 'Alice',
lastName: 'Lovelace'
}, {
createdAt: '2025-04-13T02:04:42.668Z',
likedBy: {
id: 768,
firstName: 'Bob',
lastName: 'Babbage'
We also “forgot” which of these Likes are from friends. Should we solve this by having a separate /api/post/123/friend-likes endpoint? Or should we order by friends first and include isFriend into the likes array items so we can disambiguate friends from other likes? Or should we add ?filter=friends?
Or should we include the friend likes directly into the Post to avoid two API calls?
[...]