Fully extensible Node.js REST API framework for Bookshelf.js and Hapi
Try the sample app kalamata-sample
cd
into your project and npm install hapi-kalamata
Kalamata helps you build REST APIs that run on Hapi. 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 Hapi server and hapi-kalamata
var Hapi = require('hapi');
var server = new Hapi.Server();
server.connection({ port: 3000 });
var kalamata = require('hapi-kalamata');
var api = kalamata(server);
// expose the User model
api.expose(User);
// Start Hapi server
server.start(function(err){
if (err) throw err;
console.log('Server running at:', server.info.uri);
});
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 |
### Extending the default endpoints
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:
```js
/*
* 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' });
}
});
apiRoot
option sets a prefix for all API endpoints
/* * prefixes all endpoints with `/api/v1`, * for example `/api/v1/users` */ var api = kalamata(server, { apiRoot: '/api/v1' });
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.
Gets an array of users
/*
* GET `/users`
*/
// response:
[
{ "id": 1, "name": "user1" },
{ "id": 2, "name": "user2" },
{ "id": 3, "name": "user3" }
]
/users?where={name:"user2"}
Expects the same parameters as the bookshelf.js where method
/users?load=orders,favorites
Expects a comma delimited string of relations. Calls the bookshelf.js load method method with an array of relations.
Gets a user
/*
* GET `/users/2`
*/
// response:
{ "id": 2, "name": "user2" }
/user/2?load=orders,favorites
Expects a comma delimited string of relations. Calls the bookshelf.js load method method with an array of relations.
Creates a user
/*
* POST `/users` { "name": "user4" }
*/
// response:
{ "id": 4, "name": "user4" }
Modifies a user
/*
* PUT `/users/2` { "name": "user2 MODIFIED" }
*/
// response:
{ "id": 2, "name": "user2 MODIFIED" }
Deletes a user
/*
* DELETE `/users/3`
*/
// response:
true
Gets an array of things related to a user
/*
* GET `/users/2/things`
*/
// response:
[{ "id": 3, "name": "thing3" },{ "id": 4, "name": "thing4" }]
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 a Hapi 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 });
});