Implementing Pipes2 Endpoints

Introduction

Zapp apps consume data via Pipes2 Endpoints.

A Pipes2 Endpoint is a simple REST API that responds with a JSON that conforms to the Pipes Feed JSON schema. Pipes2 Endpoints can be developed in any programming language or framework.

A good implementation of Pipes2 Endpoints will result in consumer applications that are secure & efficient as well as increase the flexibility available for editors working in Zapp Studio.

info

Users of Zapp Studio can connect UI components to any Pipes2 Endpoint, the Pipes2 Client embedded in Zapp's applications will send the endpoint additional context about the request such as user/device-specific properties.

tip

A Pipes2 Endpoint can be implemented as a middleware between the consumer applications and the backend system (i.e. CMS) or as an integral part of the backend system, for example as a wordpress plugin. When deciding about these options it's best to take into account the requirements regarding latency, scale and cost. It's often the case that legacy CMSs does not meet the scalability needed by consumer applications, for those cases Applicaster recommends to implement the Pipes2 Endpoints as a middleware on a layer that is more cost-effective to scale (for example serverless) and ensure the calls to the legacy system's API are cached.

Pipes2 Simple Endpoint Example

The following is a simple Pipes2 Endpoint implemented with expressjs that returns the static Pipes Feed JSON that is stored in the feed.json file (shown below).

const express = require('express')
const app = express()
const port = 8080;
const feed = fs.readFileSync('feed.json');
app.get('/example', (req, res) => {
res
.set('Content-Type', 'application/vnd+applicaster.pipes2+json')
.send(feed)
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
{
"id": "https://www.example.com/path/of/example/endpoint/myFeedID",
"type": { "value": "feed" },
"title": "Example feed",
"entry": [
{
"id": "89123456701",
"title": "Example Video",
"summary": "Action · 149 Mins",
"updated": "2019-11-15T10:41:03+00:00",
"published": "2019-11-15T10:31:00+00:00",
"type": { "value": "video" },
"content": {
"src": "https://www.example.com/path/to/example/video.m3u8",
"type": "video/hls"
},
"extensions": {
"free": true,
"section": "JUST ADDED",
"duration": 9863
},
"media_group": [
{
"type": "image",
"media_item": [
{
"key": "thumbnail",
"src": "https://www.example.com/path/to/example/image.jpg",
}
]
}
]
}
]
}

Using the Path and Query Parameters

When a Pipes2 Client is instructed to fetch a feed from a given URL, the client will attempt to find a configured Pipes2 Endpoint from Zapp's data source configuration with an endpoint URL that is a prefix of the feed URL.

This means that implementations may choose to implement one endpoint for all types of data and rely on the path and query string to control the returned entries. The possible paths and query parameters are not governed by Zapp or the Pipes2 protocol except the ctx query parameter that is reserved for protocol usage.

For example, given an endpoint with the prefix of https://example.com/pipes2, a component can fetch data using the feed URL https://example.com/pipes2/shows?page=1&limit=25 or by using https://example.com/pipes2/shows/some-show/seasons?orderBy=index:asc. The following expressjs code uses both path and parameters to return the wanted result:

const express = require('express');
const app = express();
const port = 8080;
app.get('/pipes2/shows', (req, res) => {
res
.set('Content-Type', 'application/vnd+applicaster.pipes2+json')
.send(getAllShows(page: req.query.page, limit: req.query.limit));
})
app.get('/pipes2/shows/:showId/seasons', (req, res) => {
res
.set('Content-Type', 'application/vnd+applicaster.pipes2+json')
.send(getSeasonsByShowId(req.params.showId, orderBy: req.query.orderBy));
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

Using Context Parameters

Pipes2 Clients can send additional parameters to the Pipes2 Endpoint when instructed to do so. When an endpoint is configured for an application in Zapp it is configured with a list of local context parameters to be sent for the endpoint (Context Keys). These parameters will be sent to the endpoint in the ctx query string parameter. The value of ctx will be a base64URL encoded JSON containing an object whose keys are the Context Keys and their respective values in the application's context are the values.

To find out more about base64URL please refer to https://tools.ietf.org/html/rfc4648#section-5

Context parameters are provided by the Zapp SDK and the plugins that are activated in the application. See list of builtin context parameters

For example, an endpoint that uses the deviceId context parameter to customize the feed:

const express = require('express')
const base64url = require('base64url')
const app = express()
const port = 8080;
app.get('/recently-watched', (req, res) => {
const context = JSON.parse(base64url.decode(req.query.ctx))
res
.set('Content-Type', 'application/vnd+applicaster.pipes2+json')
.send(generateFeedByTimezoneOffset(context["timeZoneOffset"]))
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

Geo Restricting content with signedDeviceInfoToken context key

A common use case is to be able serve / geo block specific content according to the user's location. Zapp apps support this feature out of the box using context keys.

Once you set in Zapp the the signedDeviceInfoToken context key to an Endpoint - a signed JWT context key will be added to each of the feeds' endpoints.

On your Pipes2 server implementation you'll be able to decode / validate the JWT token and use it to serve the right content to your user.

const express = require('express')
const base64url = require('base64url')
// list of available JWT libraries can be found at - https://jwt.io/
const jwt = require('jsonwebtoken');
const app = express()
const port = 8080;
app.get('/recently-watched', async (req, res) => {
// Base64 decode the ctx query param
const context = JSON.parse(base64url.decode(req.query.ctx))
// Get the signed Device JWT
const signedDeviceInfoToken = context["signedDeviceInfoToken"];
// Get the public key from `https://di.applicast er.com/public` and store it an environment variable
const diPublicKey = process.env.DI_PUBLIC_KEY
const { country } = jwt.verify(signedDeviceInfoToken, diPublicKey);
// Return a feed with one live stream - use a different stream to server clients from United States and other to server clients outside of United States
res
.set('Content-Type', 'application/vnd+applicaster.pipes2+json')
.send({
id: 'my-feed-id',
entry: [
{
id: 'my-live-stream',
title: 'Live Stream',
content: {
// US is the country code of the United States according to ISO 3166-1 (https://en.wikipedia.org/wiki/ISO_3166-1)
src: (country === 'US') ? 'https://us-live-strem.m3u8' : 'https://rest-of-the-world-live-strem.m3u8'
type: "video/hls"
}
}
]
})
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
tip

If you want to test your server implementation, you can use the Pipes2 Feed JSON validator :

  • Add you feed URL in the URL section.
  • Click on the Add context key & value pair link.
  • Add the context key signedDeviceInfoToken on the key.
  • Go to https://di.applicaster.com/ and copy the value of the jwt key and paste it in the as the value of the context key.
  • Click on the Analyze button - you'll be able to see the feed geo targeted to your location.
  • In the case you need to test another location access the https://di.applicaster.com/ from a VPN to simulate your desired country.