pbx
v1.0.0-beta-08
Published
Modular server implementation based on Patchboard API schema
Downloads
38
Readme
PBX
Warning This is an experimental project.
PBX is a reimagining of Patchboard with the following design goals:
Support for the Patchboard API description schema
Modularization of the architecture
Simplification of request classification
Support for ES6 (mostly via generator/promise-based interfaces)
Optimizations for common development scenarios
Although PBX is currently a single library, the idea is to package and release it as several standalone libraries that can interoperate. We want to empower developers to pick and choose the tools they want to use and to ultimately be able to create their our Patchboard-based solutions.
These components include:
A validator that uses JSCK for JSON schema validation. Validators are used to validate API definitions, query parameters, and request and response bodies.
A client that uses Shred to generate HTTP API clients based on the API definition.
A builder for creating API definitions quickly, much like you can do with frameworks like Restify and Express (except the definitions are valid Patchboard API definitions and can be discovered/reflected upon).
A classifier for determining the
(resource, action)
pair associated with a given request.A context object that provides a series of helper methods for dealing with requests and responses, leveraging the API definition to do so.
A processor that provides a standard request handler for use with the Node HTTP API.
A collection of behaviors encapsulating common API scenarios, such as providing an HTTP interface to a storage backend.
Example: A Simple Blog Engine
First, let's define our API:
{Builder} = require "pbx"
builder.define "blogs",
path: "/blogs"
.post
as: "create"
creates: "blog"
builder.define "blog",
template: "/blogs/:name"
.get()
.put authorization: true
.delete authorization: true
.post
creates: "post"
authorization: true
.schema
required: ["name", "title"]
properties:
name: type: "string"
title: type: "string"
builder.define "post",
template: "/blog/:name/:key"
.get()
.put authorization: true
.delete authorization: true
.schema
required: ["key", "title", "content"]
properties:
key: type: "string"
title: type: "string"
content: type: "string"
builder.reflect()
module.exports = builder.api
This API allows to create blogs, view and update them, and delete them. We can also do the same for posts within a blog. Requests that update our blog require authorization. We also added reflection to our API, which means the Patchboard API definition is available via a GET
request to /
.
For example, if we have a blog named my-blog
and a post named pbx-example
, the API above would allow us to read that post with the following curl
command:
$ curl 'http://acmeblogging.com/blog/my-blog/pbx-example'
-H'accept:application.vnd.post+json;version=1.0
Let's serve up the API using the Node HTTP createServer
method:
{call} = require "when/generator"
{processor} = require "pbx"
api = require "./api"
call ->
(require "http")
.createServer yield (processor api, (-> {}))
.listen 8080
If we run this, we'll have an HTTP server for our API running on port 8080
on localhost
. We can even use curl
to get a description of the interface:
$ curl http://localhost:8080/ -H'accept: application/json'
Wait, though…this API doesn't actually do anything. We haven't created any behaviors to bind it to. That's what's going on with the function we're passing into the pbx
function, which returns an empty object literal: (-> {})
.
The pbx
function takes our API definition and an initializer function that returns a set of handlers for each action defined by the API.
Separation of Interface and Implementation Defining the API interface separate from the implementation is different from most other HTTP libraries. One benefit of this separation is that we can generate our implementation based on the API, or even make it dynamic (say, using JavaScript proxies).
These dynamic implementation patterns are called behaviors. Behaviors open up a variety of possibilities for implementations. In this example, we'll simply define explicit handler functions for each action. But behaviors make it possible to encapsulate an reuse common patters (like storing a resource in a database).
See the example app for more.