Fully extensible Node.js REST API framework for Bookshelf.js and Express
Try the sample app kalamata-sample
cd
into your project and npm install kalamata
Kalamata helps you build REST APIs that run on Express. It creates some standard CRUD endpoints for you, and allows you to extend these with your application specific logic.
Lets say you have a Bookshelf model called User
var User = bookshelf.Model.extend({
tableName: 'users'
});
You can use Kalamata to expose this model to your API
// set up express and kalamata
var app = require('express')();
var kalamata = require('kalamata');
var api = kalamata(app);
// expose the User model
api.expose(User);
// tell express to listen for incoming requests
app.listen(8080, function() {
console.log('Server listening on port 8080');
});
which will create these endpoints
Method | URL | Action |
---|---|---|
GET | /users |
Get all users |
GET | /users/:id |
Get a user by id |
POST | /users |
Create a new user |
PUT | /users/:id |
Update an existing user |
DELETE | /users/:id |
Delete an existing user |
You can extend the default endpoints by modifying data before or after it is saved, using before
and after
hooks. These give you access to the Express request and response objects, and the Bookshelf model instance.
Some examples:
/*
* function executes on PUT `/users/:id`
* before updated user data is saved
*/
api.beforeUpdateUser(function(req, res, user) {
// set a propety before user is saved
user.set('updated_on', Date.now());
});
/*
* function executes on GET `/users`
* before the collection of users is fetched
*/
api.beforeGetUsers(function(req, res, user) {
// add a where clause to execute when fetching users
user.where({ deleted:false });
});
/*
* function executes on GET `/users/:id`
* after a user is fetched
*/
api.afterGetUser(function(req, res, user) {
if(!isAuthenticatedUser(user)) {
// override the default user data response
res.send({ error: 'access denied' });
}
});
kalamata(expressApp, [options])
apiRoot
option sets a prefix for all API endpoints
/* * prefixes all endpoints with `/api/v1`, * for example `/api/v1/users` */ var api = kalamata(app, { apiRoot: '/api/v1' });
expose(bookshelfModel, [options])
endpointName
option sets the name of the endpoint.
Defaults to the bookshelf model's tableName property.
// sets endpoints up on `/allusers` api.expose(User, { endpointName: 'allusers' });
identifier
option sets the name of the identifier param
Defaults to
id
/* * when identifier is set to `user_id`, * a request to `/users/32` will fetch * the user with `user_id = 32` */ api.expose(User, { identifier: 'user_id' });
modelName
option sets the name of the model
Defaults to the endpoint name capitalized with the
s
removed (users
->User
)
collectionName
options sets the name for a collection of model instances
Defaults to the endpoint name capitalized (
users
->Users
)
Calling expose
on a model will create a set of default CRUD endpoints. Here are the default endpoints, assuming that api.expose(User)
was called.
/users
Gets an array of users
/*
* GET `/users`
*/
// response:
[
{ "id": 1, "name": "user1" },
{ "id": 2, "name": "user2" },
{ "id": 3, "name": "user3" }
]
where
parameter includes a where clause in the query/users?where={name:"user2"}
Expects the same parameters as the bookshelf.js where method
load
parameter will load related models and include them in the response/users?load=orders,favorites
Expects a comma delimited string of relations. Calls the bookshelf.js load method method with an array of relations.
/users/:identifier
Gets a user
/*
* GET `/users/2`
*/
// response:
{ "id": 2, "name": "user2" }
load
parameter will load related models and include them in the response/user/2?load=orders,favorites
Expects a comma delimited string of relations. Calls the bookshelf.js load method method with an array of relations.
/users
Creates a user
/*
* POST `/users` { "name": "user4" }
*/
// response:
{ "id": 4, "name": "user4" }
/users/:identifier
Modifies a user
/*
* PUT `/users/2` { "name": "user2 MODIFIED" }
*/
// response:
{ "id": 2, "name": "user2 MODIFIED" }
/users/:identifier
Deletes a user
/*
* DELETE `/users/3`
*/
// response:
true
/users/:identifier/things
Gets an array of things related to a user
/*
* GET `/users/2/things`
*/
// response:
[{ "id": 3, "name": "thing3" },{ "id": 4, "name": "thing4" }]
/users/:identifier/things
Relates a thing to a user
/*
* POST `/users/2/things` { "id": "3" }
*/
// response:
{}
Hooks let you extend and override default endpoint behaviors.
before
hooks are executed before the default database action, such as fetch, save, or delete. after
hooks are executed after all database actions are complete.
Hook names are generated based on endpoint configurations. This list is based on a /users
endpoint where modelName = User
and collectionName = Users
Hook Name | Request | Arguments |
---|---|---|
beforeGetUsers |
GET /users |
[req, res, userModel] |
afterGetUsers |
GET /users |
[req, res, userCollection] |
beforeGetUser |
GET /users/:id |
[req, res, userModel] |
afterGetUser |
GET /users/:id |
[req, res, userModel] |
beforeCreateUser |
POST /users |
[req, res, userModel] |
afterCreateUser |
POST /users |
[req, res, userModel] |
beforeUpdateUser |
PUT /users/:id |
[req, res, userModel] |
afterUpdateUser |
PUT /users/:id |
[req, res, userModel] |
beforeDeleteUser |
DELETE /users/:id |
[req, res, userModel] |
afterDeleteUser |
DELETE /users/:id |
[req, res, userModel] |
beforeGetRelatedThings |
GET /users/:id/things |
[req, res, thingModel] |
afterGetRelatedThings |
GET /users/:id/things |
[req, res, thingsCollection] |
beforeRelatedThing |
POST /users/:id/things |
[req, res, userModel] |
afterRelateThing |
POST /users/:id/things |
[req, res, userModel, thingModel] |
req
and res
are an Express request and response
userModel
is an instance of a bookshelf model
userCollection
is an instance of a bookshelf collection
api.beforeCreateUser(function(req, res, user) {
// do stuff before the user is created
});
api.afterCreateUser(function(req, res, user) {
// do stuff after the user is created
});
Because you have the full power of Express and Bookshelf within your hooks, you have total control over how the Kalamata endpoints behave. Here are some examples:
If the server receives a POST /users { "name":"Joey" }
request:
/*
* The user model can be manipulated before it is saved.
*
* When this hook is finished executing,
* `{ "name":"Joey McGee" }` will be saved
*
*/
api.beforeCreateUser(function(req, res, user) {
var userName = user.get('name');
user.set({name:userName + ' McGee'});
});
/*
* After the user is created, the response can be manipulated.
*
* When this hook is finished executing, the server will
* respond with `{ "name":"Joey", "lastName":"McGee" }`
*
* The changes to the user will not be saved, because this hook
* is executed after the user is saved
*
*/
api.afterCreateUser(function(req, res, user) {
var nameSplit = user.get('name').split(' ');
user.set({
name: nameSplit[0],
lastName: nameSplit[1]
});
});
If the server receives a GET /user/5
request, but you don't want to respond with the user's data:
/*
* Send a response from the before hook
*
* Once a response is sent, Kalamata will not execute
* any of the default actions, including after hooks.
*
*/
api.beforeGetUser(function(req, res, user) {
if(user.get('id') == 5) {
res.send({ error: "access denied" });
}
});
api.afterGetUser(function(req, res, user) {
// will not be executed on requests for `user/5`
});
If the server receives a DELETE /user/5
request, Kalamata will call user.destroy()
by default. You can override this default behavior by returning a promise from the before hook:
/*
* Call a function that returns a promise, and have the
* hook function return the result of that promise
*
* Kalamata will not execute the default action,
* which in this case would have been `user.destroy()`
*
* Flag the user as deleted with a `deleted=true` property
*/
api.beforeDeleteUser(function(req, res, user) {
return user.save({ deleted: true });
});