Podium.io
Easy server side composition of microfrontends
Autonomous development
By adopting a simple manifest, teams can develop and serve parts of a web page in isolation as if one where developing and hosting a full site. These isolated parts, aka Podlets, can easily be developed in any technology stack or one can opt in to using the node.js Podium library with your favorite HTTP framework of choice.
Powerful composition
Podium makes it easy, yet flexible, to compose parts developed in isolation into full complex pages, aka Layouts. Page compostion is done programmatically instead of through config or markup providing much more power and freedom to make compostion suit every need one might have.
Contract based
Composition with Podium is done over HTTP but between the isolated parts and the composition layer there is a strong contract. This ensures that the isolated parts always has a set of key, request bound, properties which can be of value to them to operate while the composition layer has usefull info about each isolated part it can use when composing.
Podlets
Podlets (page fragments) are standalone HTTP services developed and run in isolation. Podlets can be written in any language, but Podium comes with a @podium/podlet module for easy development of Podlets in node.js.
This is a Podlet serving a HTML endpoint responding to the locale it get from a Layout:
- Express
- Hapi
- Fastify
- Http
import express from 'express';
import Podlet from '@podium/podlet';
const app = express();
const podlet = new Podlet({
name: 'myPodlet',
version: '1.0.0',
pathname: '/',
development: true,
});
app.use(podlet.middleware());
app.get(podlet.content(), (req, res) => {
if (res.locals.podium.context.locale === 'nb-NO') {
return res.status(200).podiumSend('<h2>Hei verden</h2>');
}
res.status(200).podiumSend(`<h2>Hello world</h2>`);
});
app.get(podlet.manifest(), (req, res) => {
res.status(200).json(podlet);
});
app.listen(7100);
import HapiPodlet from '@podium/hapi-podlet';
import Podlet from '@podium/podlet';
import Hapi from 'hapi';
const app = Hapi.Server({
host: 'localhost',
port: 7100,
});
const podlet = new Podlet({
name: 'myPodlet',
version: '1.0.0',
pathname: '/',
development: true,
});
app.register({
plugin: new HapiPodlet(),
options: podlet,
});
app.route({
method: 'GET',
path: podlet.content(),
handler: (request, h) => {
if (request.app.podium.context.locale === 'nb-NO') {
return h.podiumSend('<h2>Hei verden</h2>');
}
return h.podiumSend('<h2>Hello world</h2>');
},
});
app.route({
method: 'GET',
path: podlet.manifest(),
handler: (request, h) => JSON.stringify(podlet),
});
app.start();
import fastifyPodlet from '@podium/fastify-podlet';
import fastify from 'fastify';
import Podlet from '@podium/podlet';
const app = fastify();
const podlet = new Podlet({
name: 'myPodlet',
version: '1.0.0',
pathname: '/',
development: true,
});
app.register(fastifyPodlet, podlet);
app.get(podlet.content(), async (request, reply) => {
if (reply.app.podium.context.locale === 'nb-NO') {
reply.podiumSend('<h2>Hei verden</h2>');
return;
}
reply.podiumSend('<h2>Hello world</h2>');
});
app.get(podlet.manifest(), async (request, reply) => {
reply.send(podlet);
});
const start = async () => {
try {
await app.listen(7100);
app.log.info(`server listening on ${app.server.address().port}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
start();
import { HttpIncoming } from '@podium/utils';
import Podlet from '@podium/podlet';
import http from 'http';
const podlet = new Podlet({
name: 'myPodlet',
version: '1.0.0',
pathname: '/',
development: true,
});
const server = http.createServer(async (req, res) => {
let incoming = new HttpIncoming(req, res);
incoming = await podlet.process(incoming);
if (incoming.url.pathname === podlet.manifest()) {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.setHeader('podlet-version', podlet.version);
res.end(JSON.stringify(podlet));
return;
}
if (incoming.url.pathname === podlet.content()) {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.setHeader('podlet-version', podlet.version);
if (incoming.context.locale === 'nb-NO') {
res.end(podlet.render(incoming, '<h2>Hei verden</h2>'));
return;
}
res.end(podlet.render(incoming, '<h2>Hello world</h2>'));
return;
}
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end('Not found');
});
server.listen(7100);
Start the Podlet and it will be accessable on http://localhost:7100/
Layouts
Layouts are standalone HTTP services which on run time compose one or multiple Podlets (page fragments) into full webpages. Layouts are written in node.js with the @podium/layout module.
This is a Layout which will include two Podlets:
- Express
- Hapi
- Fastify
- Http
import express from 'express';
import Layout from '@podium/layout';
const layout = new Layout({
name: 'myLayout',
pathname: '/',
});
const podletA = layout.client.register({
name: 'myPodletA',
uri: 'http://localhost:7100/manifest.json',
});
const podletB = layout.client.register({
name: 'myPodletB',
uri: 'http://localhost:7200/manifest.json',
});
const app = express();
app.use(layout.middleware());
app.get(layout.pathname(), async (req, res, next) => {
const incoming = res.locals.podium;
const [a, b] = await Promise.all([
podletA.fetch(incoming),
podletB.fetch(incoming),
]);
res.podiumSend(`
<section>${a.content}</section>
<section>${b.content}</section>
`);
});
app.listen(7000);
import HapiLayout from '@podium/hapi-layout';
import Layout from '@podium/layout';
import Hapi from 'hapi';
const app = Hapi.Server({
host: 'localhost',
port: 7000,
});
const layout = new Layout({
name: 'myLayout',
pathname: '/',
});
const podletA = layout.client.register({
name: 'myPodletA',
uri: 'http://localhost:7100/manifest.json',
});
const podletB = layout.client.register({
name: 'myPodletB',
uri: 'http://localhost:7200/manifest.json',
});
app.register({
plugin: new HapiLayout(),
options: layout,
});
app.route({
method: 'GET',
path: layout.pathname(),
handler: (request, h) => {
const incoming = request.app.podium;
const [a, b] = await Promise.all([
podletA.fetch(incoming),
podletB.fetch(incoming),
]);
h.podiumSend(`
<section>${a.content}</section>
<section>${b.content}</section>
`);
},
});
app.start();
import fastifyLayout from '@podium/fastify-layout';
import fastify from 'fastify';
import Layout from '@podium/layout';
const app = fastify();
const layout = new Layout({
name: 'myLayout',
pathname: '/',
});
const podletA = layout.client.register({
name: 'myPodletA',
uri: 'http://localhost:7100/manifest.json',
});
const podletB = layout.client.register({
name: 'myPodletB',
uri: 'http://localhost:7200/manifest.json',
});
app.register(fastifyLayout, layout);
app.get(layout.pathname(), async (request, reply) => {
const incoming = reply.app.podium;
const [a, b] = await Promise.all([
podletA.fetch(incoming),
podletB.fetch(incoming),
]);
reply.podiumSend(`
<section>${a.content}</section>
<section>${b.content}</section>
`);
});
const start = async () => {
try {
await app.listen(7000);
app.log.info(`server listening on ${app.server.address().port}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
}
start();
import { HttpIncoming } from '@podium/utils';
import Layout from '@podium/layout';
import http from 'http';
const layout = new Layout({
name: 'myLayout',
pathname: '/',
});
const podletA = layout.client.register({
name: 'myPodletA',
uri: 'http://localhost:7100/manifest.json',
});
const podletB = layout.client.register({
name: 'myPodletB',
uri: 'http://localhost:7200/manifest.json',
});
const server = http.createServer(async (req, res) => {
let incoming = new HttpIncoming(req, res);
incoming = await layout.process(incoming);
if (incoming.url.pathname === layout.pathname()) {
const [a, b] = await Promise.all([
podletA.fetch(incoming),
podletB.fetch(incoming),
]);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end(podlet.render(incoming, `
<section>${a.content}</section>
<section>${b.content}</section>
`));
return;
}
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end('Not found');
});
server.listen(7000);
Start the Layout and it will be accessable on http://localhost:7000/