npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

phrapi

v1.0.0

Published

A Promise Handling REST API

Downloads

3

Readme

Phrapi Build Status

A Promise-handling REST API

Intent

Phrapi is intended to be simple, composable, and deal exclusively with JSON-based REST.

The goal is not to build a Hapi-level framework with authentication and templating baked in. If you need those capabilities, use Hapi. If, instead, you just need something light, fast, and simple, maybe give Phrapi a try.

About

I've spent a lot of time working with frameworks like Hapi and Express and I really like both. I tend to prefer Hapi, but there are occasions where Express is the right answer.

I found that the way that I was building APIs began to follow a basic pattern that required me to reproduce the same code structures with predictable regularity. That pattern is:

  • Routes define paths and invoke handlers
  • Handlers contain the business logic of the API and invoke controllers to access data
  • Controllers CRUD data

Within this pattern, I always made my controllers return promises so that they could be composed into the flows determined by the routes and executed by the handlers and I really liked it. It was easy to make big changes and still have a codebase that was easy to read for someone that is unfamiliar with the code.

What I felt like I was missing was the ability to have the handlers also deal in Promises. The structure of the handler function is typically determined by the route() method of whatever framework you're using and generally follows the (request, response[, next]) => {} signature. To compose a handler, you typically have to find a way to orchestrate the assignment of data to the request or response context. Once you've done that, to make it work with Promises, you've got to start all function calls by returning a new Promise().

Example

// handlers.js
'use strict';

const handlers = {
  ping(ctx, resolve, reject) {
    resolve({ pong: new Date() });
  },
  foo(ctx, resolve, reject) {
    let { params } = ctx;

    resolve({ foo: 'URL {bar} param = ' + params.bar });
  },
  bar(ctx, resolve, reject) {
    let { resolved } = ctx;

    resolve({ bar: 'foo resolved = ' + resolved.foo });
  }
};

export default handlers;
// routes.js
'use strict';

import { ping, foo, bar } from './handlers';

const routes = [{
  method: 'get',
  path: '/ping',
  flow: [ ping ]
}, {
  method: 'get',
  path: '/foo/{bar}',
  flow: [ foo, bar ]
}];

export default routes;
// server.js
'use strict';

import Phrapi from 'phrapi';
import routes from './routes';

const router = new Phrapi.Router({ routes });

const api = new Phrapi.Server({ router });

api.start(3000, (err) => {
  if (err)
    throw err;

  console.log('listening:', api.info);
});
$ curl localhost:3000/ping
{"pong":"2016-02-25T17:12:46.504Z"}

$ curl localhost:3000/foo/baz
{"bar":"foo resolved = URL {bar} param = baz"}

Phrapi.Server

The Phrapi.Server() call will return an object with five properties:

  1. start(port, callback)
  2. stop()
  3. route({ route })
  4. test({ method, path[, payload] })
  5. info

Server() accepts an optional { router: new Phrapi.Router() } argument making it possible to compose your routing externally to the server code.

Server.route() is a convenience call to Router.route().

Server.test({ method, path[, payload] })

In an effort to make testing as easy as possible, the test() method was added to the Server. This will programmatically call server.start(0, () => {}) and send an actual http.request() call to trigger the route.

'use strict';

import Phrapi from '../';
import routes from './routes';

const router = new Phrapi.Router({ routes });

const server = new Phrapi.Server({ router });

server.test({
  method: 'get',
  path: '/ping'
}).then(json => {
  // assert on json
  console.log('you got back:', json);
}).catch(err => {
  console.log('There was an error!');
  console.log(err.message);
  process.exit(1);
});

server.test({
  method: 'post',
  path: '/foo',
  payload: { blargh: 'honk' }
}).then(json => {
  // assert on json
  console.log('you got back:', json);
}).catch(err => {
  console.log('There was an error!');
  console.log(err.message);
  process.exit(1);
});

Phrapi.Router

The Phrapi.Router() call returns a ... wait for it ... router object with three properties:

  1. routes[]
  2. route({Route})
  3. find(method, path)

Router() accepts an optional { routes: [{Route}], pathPrefix: '/v1' } argument so that you can also compose routes externally and mount them on a specific prefix

const router = new Phrapi.Router({ pathPrefix: '/v1' });

router.route({
  method: 'get',
  path: '/foo',
  flow: [(ctx, resolve, reject) => resolve({ foo: true }) ]
});

const server = new Phrapi.Server({ router });

server.start(3000, (err) => {
  if (err)
    throw err;

  console.log('server started:', server.info);
});
$ curl localhost:3000/v1/foo
{"foo":true}

Flows

The flow property on a Route is where you compose your handlers to build a response. The array is processed serially unless you group handler functions within an array.

const routes = [{
  method: 'get',
  path: '/foo',
  // when foo resolves, bar is called
  flow: [ foo, bar ]
}, {
  method: 'get',
  path: '/baz',
  // when foo resolves, bar and baz are called with Promise.all
  flow: [ foo, [ bar, baz ] ]
}];

Flow Handlers

A flow handler expects the signature (ctx, resolve, reject) => {}.

resolve() and reject() are the standard ES6 Promise calls.

The ctx argument is the context of the request/response that has been decorated with a few properties to make processing simple:

  • params - An object with key/value pairs parsed from the URL params defined in the route
  • query - An object with key/value pairs parsed from the query string
  • payload - The contents of any JSON sent with the request
  • resolved - When a flow has multiple handlers, the results of any previously resolved handlers are made available in the ctx.resolved object. This makes it possible to quickly compose responses that require multiple database calls to construct the response JSON. Think of this as being very similar to the pre option in Hapi route congifuration, except that it's built in by default.
  • reply - CAUTION The reply property gives you access to tinker with the response HTTP status code, headers, and payload. You should not mutate payload through this interface! To mutate the response payload, use the resolve({}) interface. If you need to change the code or add headers, however, this is the place to do that.

Whatever the final handler resolves is what becomes the response JSON.

  • request - The raw HTTP request
  • response - The raw HTTP response. While you 'can' make response.end() calls here, you 'should' rely on the resolve() and reject() process to ensure that your composed routes function as expected.

Phrapi.Errors

The Phrapi.Errors object has methods for easily composing standard HTTP errors. It's very much inspired by Boom.

const router = new Phrapi.Router({ pathPrefix: '/v1' });

router.route({
  method: 'get',
  path: '/error',
  flow: [(ctx, resolve, reject) => reject(Phrapi.Errors.invalidRequest('go away!')) ]
});

const server = new Phrapi.Server({ router });

server.start(3000, (err) => {
  if (err)
    throw err;

  console.log('server started:', server.info);
});
$ curl localhost:3000/v1/error
{"code":400,"message":"go away!"}

Versions

  • 1.0.0 - Breaking change - Converted handler signature to (ctx, resolve, reject) where ctx = { request, response, reply, params, query, payload, resolved }
  • 0.1.1 - Readme fix
  • 0.1.0 - Added Server.stop() and Server.test()
  • 0.0.2 - Added reply to request in Handler signature
  • 0.0.1 - Initial release

TODO

  • Allow for custom 404