From Point-to-Point Calls to Composable Stacks
The monolithic application is no longer the default shape of software. Modern systems are assembled from specialized services that each own a slice of functionality — content, search, commerce, payments — and the way those pieces talk to one another has been decades in the making. Tracing that path helps explain why today’s architectures, particularly those built around headless CMS platforms, look the way they do.
The Early Years: RPC and Its Legacy
When networked computers first began exchanging data, the mechanisms were direct and uncomplicated: file transfers over FTP or raw communication over TCP/IP sockets. Those approaches sufficed for straightforward tasks but fractured under the demands of more complex interactions.
The shift came with Remote Procedure Calls (RPC) in the 1980s. RPC let developers invoke procedures on remote machines as if they were local functions, hiding much of the networking complexity behind a familiar call-and-response interface. The model was simple: a client serializes a procedure call with its parameters and sends it to a server, which deserializes and executes the procedure before returning a serialized response. RPC could run synchronously or asynchronously, and modern descendants such as gRPC extended the pattern to support streaming and bidirectional communication.
Here, for instance, a gRPC service named Calculator defines two methods: one conventional RPC that takes a Numbers message and returns a Result, and another that streams back multiple Result messages.
// protobuf
service Calculator {
rpc Calculate(Numbers) returns (Result);
rpc CalculateStream(Numbers) returns (stream Result);
}
Web Services and the API Landscape
The late 1990s and early 2000s brought Web Services and Service-Oriented Architecture (SOA) into the enterprise mainstream. SOAP standardized integration but at the cost of heavy complexity and verbosity. That friction drove the industry toward simpler styles, and REST APIs soon became the dominant form of web communication.
RESTful APIs
REST (Representational State Transfer), first described by Roy Fielding in 2000, is an architectural style built on the Web’s own standards. Its constraints — a clear interface separating client and server, statelessness, cacheable responses, loose coupling — align with the web’s goals of performance, scalability, reliability, and visibility. In practice, most REST implementations encode request and response messages in JSON.
// Request
async function fetchUserData() {
const response = await fetch('https://api.example.com/users/123');
const userData = await response.json();
return userData;
}
// Response
{
"id": "123",
"name": "John Doe",
"_links": {
"self": { "href": "/users/123" },
"orders": { "href": "/users/123/orders" },
"preferences": { "href": "/users/123/preferences" }
}
}
GraphQL
GraphQL began as an internal Facebook project in 2012 and was open-sourced in 2015. It emerged from the pain of building complex mobile applications against REST endpoints, where clients often received too much data or had to make multiple round trips to gather enough. GraphQL offers a type system and a declarative query language; the client specifies exactly the shape of the data it needs, and the server responds accordingly. It has found particular traction in nested UI structures, mobile apps, and microservices environments, backed by a growing ecosystem of tooling.
Webhooks and Event-Driven Design
Request-response protocols have a weakness when applications need real-time behavior. E-commerce systems must adjust inventory the moment a purchase lands, and content platforms need to invalidate caches on publish. Polling for such changes is inefficient and wasteful.
Webhooks flip the model: the server pushes a notification to a subscribed client or service when an event occurs. Event-driven architectures go a step further by decoupling components entirely — services publish and subscribe to events asynchronously, which improves scalability and responsiveness.
A lightweight Node.js server built with Fastify illustrates the pattern. The server listens on /webhook, inspects the type field of an incoming JSON payload, and refreshes a cache when the event is content.published.
import fastify from 'fastify';
const server = fastify();
server.post('/webhook', async (request, reply) => {
const event = request.body;
if (event.type === 'content.published') {
await refreshCache();
}
return reply.code(200).send();
});
Composable Architecture and Headless CMS
This progression of integration patterns has reshaped application design. Instead of one large system trying to do everything, teams now compose applications from best-of-breed services. Headless CMS platforms are a natural fit for this model because they separate content management from content delivery. Content can be written once and served to any frontend through an API, allowing teams to reuse content across channels, scale components independently, and select the most appropriate technology for each part of the stack.
Storyblok is one such headless CMS. It exposes content through both REST and GraphQL APIs, supports webhooks for a wide range of events, and provides a visual editor for content teams. A typical integration begins with a content delivery service that queries the CMS through its open-source JavaScript client.
import StoryblokClient from "storyblok-js-client";
class ContentDeliveryService {
constructor(private storyblok: StoryblokClient) {}
async getPageContent(slug: string) {
const { data } = await this.storyblok.get(`cdn/stories/${slug}`, {
version: 'published',
resolve_relations: 'featured-products.products'
});
return data.story;
}
async getRelatedContent(tags: string[]) {
const { data } = await this.storyblok.get('cdn/stories', {
version: 'published',
with_tag: tags.join(',')
});
return data.stories;
}
}
A Multi-Service Integration Example
To see how these pieces fit together, consider an e-commerce platform that combines Storyblok for content, Shopify for inventory and orders, Algolia for product search, and Stripe for payment processing. Each service is configured separately with its own access tokens, and the frontend orchestrates calls across them.
After initializing each client, a React component can accept a productSlug, fetch the product content from Storyblok (using a blok that stores the product_id), pull inventory data from Shopify, and query Algolia for related products.
import StoryblokClient from "storyblok-js-client";
import { algoliasearch } from "algoliasearch";
import Client from "shopify-buy";
const storyblok = new StoryblokClient({
accessToken: "your_storyblok_token",
});
const algoliaClient = algoliasearch(
"your_algolia_app_id",
"your_algolia_api_key",
);
const shopifyClient = Client.buildClient({
domain: "your-shopify-store.myshopify.com",
storefrontAccessToken: "your_storefront_access_token",
});
async function fetchProduct() {
// get product from Storyblok
const { data } = await storyblok.get(`cdn/stories/${productSlug}`);
// fetch inventory from Shopify
const shopifyInventory = await shopifyClient.product.fetch(
data.story.content.product_id
);
// fetch related products using Algolia
const { hits } = await algoliaIndex.search("products", {
filters: `category:${data.story.content.category}`,
});
}
Local component state holds the results from these parallel requests:
const [productData, setProductData] = useState(null);
const [inventory, setInventory] = useState(null);
const [relatedProducts, setRelatedProducts] = useState([]);
useEffect(() =>
// ...
// combine fetchProduct() with setState to update the state
// ...
fetchProduct();
}, [productSlug]);
The rendered template then combines the content, inventory, and search data into the final product page:
<h1>{productData.content.title}</h1>
<p>{productData.content.description}</p>
<h2>Price: ${inventory.variants[0].price}</h2>
<h3>Related Products</h3>
<ul>
{relatedProducts.map((product) => (
<li key={product.objectID}>{product.name}</li>
))}
</ul>
Purchase events can be handled asynchronously. A Fastify-based server listens for shop events and integrates with Stripe to process checkouts:
const stripe = require('stripe')
module.exports = async function plugin (app, opts) {
const stripeClient = stripe(app.config.STRIPE_PRIVATE_KEY)
server.post('/create-checkout-session', async (request, reply) => {
const session = await stripeClient.checkout.sessions.create({
line_items: [...], // from request.body
mode: 'payment',
success_url: "https://your-site.com/success",
cancel_url: "https://your-site.com/cancel",
})
return reply.redirect(303, session.url)
})
// ...
Because each service is independent, the application stays modular. Business goals such as performance, scalability, and flexibility are met not by a single platform but by the composition itself — with each piece smaller, simpler, and easier to maintain than a monolithic equivalent would be.
The Takeaway
The modern integrations between headless CMSs and specialized web services reflect the core lesson of this evolution: robust, responsive applications are built by connecting well-defined APIs rather than by consolidating everything into one system. Understanding RPC, REST, GraphQL, and event-driven patterns is not just historical knowledge. It is the practical foundation for composing the high-performance, decoupled systems that contemporary development demands.



