Automatic RESTful API enabler for KeystoneJS
This module allows you to easily expose your keystone models through a REST API. It allows for very granular control of editable fields, population, filters and more through configuration.
- Extremely easy setup
- Granular control/configuration
- Route configuration by keystone list, instead of path
- Versatile API
$ npm install --save restful-keystone
Let's assume we've got an Article
list set up in models
. We only need a few lines to expose it in a REST API.
// file: routes/index.js
var keystone = require('keystone');
// Pass your keystone instance to the module
var restful = require('restful-keystone')(keystone);
// ...
exports = module.exports = function( app ){
//Explicitly define which lists we want exposed
restful.expose({
Article : true
}).start();
}
Yep. That's it.
GET /api/articles
Status: 200 OK
{
"articles": [
{
"_id": "54e2f3d7a21780d7a097ce8d",
"title": "Lorem Ipsum",
"author": null,
"categories": [],
"content": {
"brief": "<p>Lorem Ipsum</p>",
"extended": "<p>Dolor sit amet</p>"
},
"state": "draft"
},
{
"_id": "54e2f411a21780d7a097ce8e",
"title": "Just a small Test",
"publishedDate": "2015-02-16T23:00:00.000Z",
"author": null,
"categories": [],
"content": {
"brief": "<p>This is a test</p>",
"extended": "<p>To make sure restful-keystone is functioning correctly</p>"
},
"state": "published"
}
]
}
By default it will setup these routes:
GET /api/<collection>
: alist
operation; returns all of the resourcesPOST /api/<collection>
: acreate
operation; creates a new resource in the collectionGET /api/<collection>/:id
: aretrieve
operation; retrieves a single resource from the collectionPATCH /api/<collection>/:id
: anupdate
operation; updates the state of the resource with the values from the payloadDELETE /api/<collection>/:id
: aremove
operation; removes the resource from the collection
The module itself requires a keystone
instance to be passed to it:
var restful = require("restful-keystone")(keystone);
However, you can declare the root
of your api here as well:
var restful = require("restful-keystone")(keystone, {
root: "/api/v1"
});
Will host your REST API at /api/v1
. Default value is a plain /api
.
Allows you to fully configure your exposed lists. You have to pass a truthy entry for each list you want exposed:
restful.expose({
Article: true,
User: true
});
By default lists aren't exposed for security reasons, which is why you need to tell restful-keystone explicitly you want a list enabled in the REST API.
You have to use the list name as the identifier, i.e. it's the value you passed to keystone.list
, e.g.
// file: models/articles.js
// ...
var Article = new keystone.list("Article", {
//...
});
There's a number of options you can pass to expose
for further configuration:
{Boolean|String|String[]} default: true
Allows you to configure which methods are allowed to be used on the resource, by default it's the full range.
You can supply the methods as a single string, e.g. "retrieve list"
or as an array of strings, e.g. ["retrieve", "list"]
. When provided with a boolean value it sets all methods on or off.
restful.expose({
Article : {
methods: ["retrieve", "update", "remove"]
}
});
restful.expose({
Article : {
methods: false // no methods allowed
}
});
Possible values:
"create"
: allows aPOST
on a collection of resources, e.g.POST /api/posts
. Creates a resource in the collection."list"
: allows aGET
on a collection of resources, e.g.GET /api/posts
. Retrieves a list of resources from a collection."retrieve"
: allows aGET
on a specific resource, e.g.GET /api/posts/54e2f3d7a21780d7a097ce8d
. Retrieves a specific resource from a collection."update"
: allows aPATCH
on a specific resource, e.g.PATCH /api/posts/54e2f3d7a21780d7a097ce8d
. Updates the state of a specific resource in a collection."remove"
: allows aDELETE
on a specific resource, e.g.DELETE /api/posts/54e2f3d7a21780d7a097ce8d
. Removes a specific resource from a collection.
{Boolean|String|String[]} default: true
Configures which fields of the resource will be shown, by default all fields declared in the schema (except virtuals) are shown. Again, provide them as a single space-delimited string, an array of strings or toggle them all on or off with a boolean.
restful.expose({
Article : {
show : "title content"
}
});
restful.expose({
Article : {
show : ["title", "author", "publishedDate", "content.brief"]
}
});
{Boolean|String|String[]} default: "true"
Idem to show
, except it configures which fields are editable. All fields passed to a request that are not listed in edit
will simply be ignored, even if they exist in the schema.
{Boolean|String} default: "<%=name%>"
Used for enveloping the results (which is definitely best practice, especially when multiple resources are returned).
By default this will be the singular or plural version of the list name, e.g. "articles"
or "article"
.
GET /api/articles
Status: 200 OK
{
"articles": []
}
GET /api/articles/54e2f411a21780d7a097ce8e
Status: 200 OK
{
"article": {
"_id": "54e2f411a21780d7a097ce8e",
"title": "Just a small Test",
"publishedDate": "2015-02-16T23:00:00.000Z",
"author": null,
"categories": [],
"content": {
"brief": "<p>This is a test</p>",
"extended": "<p>To make sure restful-keystone is functioning correctly</p>"
},
"state": "published"
}
}
When you pass it a plain string it will be used as-is:
restful.expose({
Article : {
envelop: "results"
}
});
Will envelop all request results in "results"
:
GET /api/articles
Status: 200 OK
{
results: [
// articles
]
}
If however, you pass it an ERB-style interpolate delimiter string it will be substituted with whatever list or configuration value you want. E.g.
restful.expose({
Article : {
envelop: "<%=path%>"
}
});
Will envelop all results (even those of requests on single resources) in a field with the same name as the path
value of the list, e.g. "articles"
:
GET /api/articles/54e2f411a21780d7a097ce8e
Status: 200 OK
{
"articles": {
"_id": "54e2f411a21780d7a097ce8e",
"title": "Just a small Test",
"publishedDate": "2015-02-16T23:00:00.000Z",
"author": null,
"categories": [],
"content": {
"brief": "<p>This is a test</p>",
"extended": "<p>To make sure restful-keystone is functioning correctly</p>"
},
"state": "published"
}
}
You can turn enveloping off altogether by passing it a boolean false
.
{Object}
You can set permanent filtering on a collection with filter
. Pass it any key/value pair to automatically restrict all operations to documents that pass the condition.
restful.expose({
Article : {
filter : {
state: "published"
}
}
});
Will only operate on Article documents that have a "published"
value in "state"
.
{Boolean|String|String[]} default: false
Allows automatic population of relationship fields.
restful.expose({
Article : {
populate : "author"
}
});
GET /api/articles/54e2f411a21780d7a097ce8e
Status: 200 OK
{
"article": {
"_id": "54e2f3d7a21780d7a097ce8d",
"title": "Lorem Ipsum",
"author": {
"_id": "543f8abd6f0a6bb721653954",
"name": {
"last": "User",
"first": "Admin",
"full": "Admin User"
},
"email": "[email protected]"
},
"content": {
"brief": "<p>Lorem Ipsum</p>",
"extended": "<p>Dolor sit amet</p>"
}
}
}
{String} default: the plural name of the list
Allows you to configure the path at which the resources will be expose.
restful.expose({
Article : {
path : "news"
}
});
GET /api/news/54e2f411a21780d7a097ce8e
Status: 200 OK
{
"article": {
"_id": "54e2f3d7a21780d7a097ce8d",
"title": "Lorem Ipsum",
"author": {
"_id": "543f8abd6f0a6bb721653954",
"name": {
"last": "User",
"first": "Admin",
"full": "Admin User"
},
"email": "[email protected]"
},
"content": {
"brief": "<p>Lorem Ipsum</p>",
"extended": "<p>Dolor sit amet</p>"
}
}
}
before
and after
allow you to register middleware functions that are executed ... umm ... before and after the response creation.
Obviously this could be achieved by using the usual app.get("/api/articles", someMiddleware)
as well, but these methods allow you to configure it by list name instead of path.
restful.expose({
Article: true
}).before({
Article: requireAdmin
});
This will call the requireAdmin
middleware function before the response is generated, for any of the methods: "retrieve", "list", ...
Obviously you can configure it to be executed for specific methods only:
restful.expose({
Article: true
}).before({
Article: {
update: requireAdmin,
remove: [requireAdmin, resourceExists],
create: [requireAdmin, limitNotReached]
}
});
Either provide the middleware by method as in the above example, but if you want to execute the same middleware on several methods at once you can pass them separately:
restful.expose({
Article: true
}).before("update remove create", {
Article: requireAdmin
});
This will execute requireAdmin
only for the "update", "remove" and "create" methods (i.e. not for "list" and "retrieve")
As you saw in the above examples you can pass a function or an array of functions in all occasions.
Multiple calls to before
(or after
) will merge the configurations:
restful.expose({
Article: true
}).before("update remove create", {
Article: requireAdmin
}).before("update", {
Article: requireAllFields
});
Will execute requireAdmin
and requireAllFields
for the "update" method.
after
behaves identical to before
except for one thing:
By default restful-keystone will send the response, but any method that receives after
middleware will have to have additional middleware to send the response. This is to allow maximum flexibility, otherwise you wouldn't be able to manipulate the results before they're sent.
restful.expose({
Article: true
}).after("create", {
Article: function(req, res, next){
console.log("CREATED:", res.locals.body);
res.send(res.locals.status, res.locals.body);
}
});
As you can see the response and status code are stored in res.locals
This signals to restful-keystone that it should set up the routes et cetera. I.e. you're finished configuring.
restful.expose({
Article: true
}).after("create", {
Article: function(req, res, next){
console.log("CREATED:", res.locals.body);
res.send(res.locals.status, res.locals.body);
}
}).start(); // DO NOT FORGET TO START restful-keystone
A start
method was added to allow you to configure restful in any order you see fit, i.e. this is all possible:
restful.before( /* config */ )
.expose( /* config */ )
.after( /* config */ )
.start();
Or
restful
.after( /* config */ )
.expose( /* config */ )
.before( /* config */ )
.start();
Just make sure start
is the last method to be called.
All values can be passed with requests as json
bodies or as url encoded json over the query string.
PATCH /api/articles/54e2f411a21780d7a097ce8e
{
"title": "Changed my title!"
}
Status: 200 OK
{
"article": {
"_id": "54e2f3d7a21780d7a097ce8d",
"title": "Changed my title!",
"author": "543f8abd6f0a6bb721653954",
"content": {
"brief": "<p>Lorem Ipsum</p>",
"extended": "<p>Dolor sit amet</p>"
}
}
}
Is equivalent to:
PATCH /api/articles/54e2f411a21780d7a097ce8e?%7B%0D%0A%09%22title%22%3A+%22Changed+my+title%21%22%0D%0A%7D
Status: 200 OK
{
"article": {
"_id": "54e2f3d7a21780d7a097ce8d",
"title": "Changed my title!",
"author": "543f8abd6f0a6bb721653954",
"content": {
"brief": "<p>Lorem Ipsum</p>",
"extended": "<p>Dolor sit amet</p>"
}
}
}
list
requests allow the passing of a filter
value in order to do on-the-fly filtering, see above for an explanation.
All requests respond with a 200 OK
status if the request was succesful, except for remove
requests which will return 204 No Content
.
When something goes wrong appropriate errors are thrown, however restful-keystone does not provide any error handling out of the box, i.e. you need to make sure you have some kind of error handling middleware in place.
restful-keystone does NOT provide any security checks, i.e. if you expose a resource it is available to anonymous requests !! You need to set up any restrictions you want to see applied to routes yourself.
- Configuration through
json
files - Optional full adherence to jsonapi.org spec
- Pagination
- v0.3 make compatible with Keystone v0.3 (i.e. express 4)
- v0.2
- API improvements
- added
before
andafter
hooks
- v0.1 initial API
MIT © d-pac