jsonapify is a library to assist the development of JSON-API compatible APIs with NodeJS.
- Simple: jsonapify is designed around simplicity. Easy things are easy to do, hard things are possible. If you feel something can be made simpler, by all means file an issue!
- Unintrusive: ExpressJS, Restify, Connect,... No matter, jsonapify integrates nicely.
- Interoperable: By offering a common-interface across your APIs, jsonapify lets your users build great things on top of them. If you don't know yet about the JSON-API specification, you should read about it and all the oportunities it has to offer.
- Well tested: jsonapify is designed from the start with unit testing in mind. Reliability is at the core of what we do.
jsonapify detaches mongoose models from the actual representation of the resources. This allows for a lot of flexibility: as a matter of fact, declaring a non-readable field is this elegant:
var User = require('../models/User');
var userResource = new jsonapify.Resource(User, {
type: 'users',
id: new jsonapify.Property('_id'),
attributes: {
email: new jsonapify.Property('email'),
password: {
value: new jsonapify.Property('password'),
readable: false,
},
},
});
jsonapify.Runtime.addResource('User', userResource);
This is how the previous example would look in ES6:
import {Property, Resource, Runtime} from 'jsonapify';
import User from '../models';
const userResource = new Resource(User, {
type: 'users',
id: new Property('_id'),
attributes: {
email: new Property('email'),
password: {
value: new Property('password'),
readable: false,
},
},
});
Runtime.addResource('User', userResource);
HATEOAS is one of the most important principles of the REST phylosophy. jsonapify makes interconnecting your resources a piece of cake:
var User = require('../models/User');
var userResource = new jsonapify.Resource(User, {
type: 'users',
id: new jsonapify.Property('_id'),
links: {
self: {
value: new jsonapify.Template('/users/${_id}'),
writable: false,
},
},
});
jsonapify.Runtime.addResource('User', userResource);
As someone said, "nobody is an island". Resources are not islands either. Linking resources in jsonapify is as easy as you'd expect:
var User = require('../models/User');
var roleResource = require('./roles').resource;
var userResource = new jsonapify.Resource(User, {
type: 'users',
id: new jsonapify.Property('_id'),
relationships: {
role: new jsonapify.Ref('Role', 'role'),
},
});
jsonapify.Runtime.addResource('User', userResource);
Note: related resources are not subresources. Subresources are resource-like objects so tightly linked to their parent resource that they can't exist on their own. jsonapify does not support access of related resources as subresources. This is by-design.
We all know about DRY. But then, why do we keep writing the same endpoint boilerplate again and again? jsonapify offers all CRUD operations as connect-compatible middleware. That means plugging a new endpoint is as simple as it gets:
app.get('/users/', [
jsonapify.enumerate('User'),
jsonapify.errorHandler()
]);
Everything in REST is a resource. Resources can have subresources, too. That means that you can apply a READ operation (GET verb in REST terms) to a subresource. Let's see how resource addressing works in jsonapify.
- Resource chains come in the form of [(typename, [selector])+].
- Resource chain selectors are applied at request-time, and are used to select a subset of objects of the preceeding resource type.
- At this moment, selectors can get info from:
- Request params:
jsonapify.param(...)
- Request query params:
jsonapify.query(...)
- Resource parent object:
jsonapify.parent(...)
- Request params:
- There are partial and full resource chains. A full resource chain maps to a single resource object, whereas a partial resource chain (the ones missing the trailing selector) map to a subset of resource objects.
- Some jsonapify operations require full resource chains (ie: READ, UPDATE,...), while others require partial resource chains (only CREATE at this moment). Therefore, the same resource chain may be interpreted as a full or a partial one depending on the context.
For example, a READ operation with the following resource chain, directed to the URI '/groups/{group}/users/{user}', would retrieve a resource object of type 'User', with group == parent._id and name == user
, where parent
is the group the user logically belongs to:
/* full chain */ [
'UserGroup', {
name: jsonapify.param('group'),
},
'User', {
group: jsonapify.parent('_id'),
name: jsonapify.param('user'),
},
]
On the other hand, a CREATE operation with the following resource chain, directed to the same URI, would store a resource object of type 'User', with group == parent._id
and the rest of the properties from the request body:
/* partial chain */ [
'UserGroup', {
name: jsonapify.param('group'),
},
'User', {
group: jsonapify.parent('_id'),
},
]
Note: While jsonapify subresource addressing is already functional, it is not polished enough to be considered production-ready (think of error reporting, usability...) If you ever encounter a bug, please file an issue and it will get assigned a high priority.
In addition to all of the above, jsonapify also offers transaction filters. These filters enable per-request functionality, such as pagination, sparse-fields, sorting... The most common transaction filters are enabled by default, so you don't have to worry.
This library wouldn't have been possible without all the great work by the people of the JSON-API specification. Thank you guys, you're awesome!