Node.js Express middleware that uses your Mongoose schema to generate SCRUD API routes compatible with AngularJS and ngResource.
Before you begin, be sure MongoDB is installed and mongod
is running.
Install meanify as a dependency and add it to your package.json
file.
npm install meanify --save
First, define your Mongoose models and any necessary validations and indexes.
// models.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var userSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true },
password: { type: String, required: true }
});
mongoose.model('User', userSchema);
var postSchema = new Schema({
title: { type: String, required: true },
contents: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: 'User', index: true }
});
mongoose.model('Post', postSchema);
Initialize meanify's router middleware after your Mongoose models.
// server.js
var express = require('express');
var app = express();
require('./models');
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/meanify');
var meanify = require('meanify')({
path: '/api',
pluralize: true
});
app.use(meanify());
app.listen(3001);
Start up your express app using the DEBUG=meanify
param to verify your routes.
โ DEBUG=meanify node server.js
The meanify log will show the newly created endpoints.
meanify GET /api/users +0ms
meanify POST /api/users +0ms
meanify GET /api/users/:id +0ms
meanify POST /api/users/:id +0ms
meanify DELETE /api/users/:id +0ms
meanify GET /api/posts +0ms
meanify POST /api/posts +0ms
meanify GET /api/posts/:id +0ms
meanify POST /api/posts/:id +0ms
meanify DELETE /api/posts/:id +0ms
The middleware functions powering these routes may also be accessed directly for more control over route creation.
For example:
// app.use(meanify()); // Disable automatic routing.
app.get('/api/posts', meanify.posts.search);
app.post('/api/posts', meanify.posts.create);
app.get('/api/posts/:id', meanify.posts.read);
app.put('/api/posts/:id', meanify.posts.update); // Support PUT instead of POST.
// app.delete('/api/posts/:id', meanify.posts.delete); // Disable this route.
Meanify expects options to be passed in on require of the module as seen below. This builds an Express middleware router object, which is accessed by invoking the returned function.
var meanify = require('meanify')({
path: '/api',
exclude: ['Counter'],
lowercase: false,
pluralize: true,
caseSensitive: false,
strict: false,
puts: true,
relate: true
});
app.use(meanify());
The root path to be used as a base for the generated routes. Default: '/'
Array of models to exclude from middleware generation. Default: undefined
Prevents meanify from lowercasing generated route name. Default: true
Pluralizes the model name when used in the route, i.e. "user" becomes "users". Default: false
Enable case sensitivity, treating "/Foo" and "/foo" as different routes. Default: true
Enable strict routing, treating "/foo" and "/foo/" differently by the router. Default true
By default, ngResource does not support PUT for updates without making it more RESTful. This option adds PUT routes in addition to the POST routes for resource creation and update.
Experimental feature that automatically populates references on create and removes them on delete. Default: false
Object containing registration hooks for search
, create
, read
, update
, and delete
operations. See below for full hooks documentation.
For each model, five endpoints are created that handle resource search, create, read, update and delete (SCRUD) functions.
GET /{path}/{model}?{fields}{options}
The search route returns an array of resources that match the fields and values provided in the query parameters.
For example:
GET /api/posts?author=544bbbceecd047be03d0e0f7&__limit=1
If no query parameters are present, it returns the entire data set. No results will be an empty array ([]
).
Options are passed in as query parameters in the format of &__{option}={value}
in the query string, and unlock the power of MongoDB's find()
API.
Option | Description |
---|---|
limit | Limits the result set count to the supplied value. |
skip | Number of records to skip (offset). |
distinct | Finds the distinct values for a specified field across the current collection. |
sort | Sorts the record according to provided shorthand sort syntax (e.g. &__sort=-name ). |
populate | Populates object references with the full resource (e.g. &__populate=users ). |
count | When present, returns the resulting count in an array (e.g. [38] ). |
near | Performs a geospatial query on given coordinates and an optional range (in meters), sorted by distance by default. Required format: {longitude},{latitude},{range} |
Meanify also supports range queries. To perform a range query, pass in a stringified JSON object into the field on the request.
GET /api/posts?createdAt={"$gt":"2013-01-01T00:00:00.000Z"}
Using ngResource
in AngularJS, performing a range query is easy:
// Find posts created on or after 1/1/2013.
Posts.query({
createdAt: JSON.stringify({
$gte: new Date('2013-01-01')
})
});
POST /{path}/{model}
Posting (or putting, if enabled) to the create route validates the incoming data and creates a new resource in the collection. Upon validation failure, a 400
error with details will be returned to the client. On success, a status code of 201
will be issued and the new resource will be returned.
GET /{path}/{model}/{id}
The read path returns a single resource object in the collection that matches a given id. If the resource does not exist, a 404
is returned.
POST /{path}/{model}/{id}
Posting (or putting, if enabled) to the update route will validate the incoming data and update the existing resource in the collection and respond with 204
if successful. Upon validation failure, a 400
error with details will be returned to the client. A 404
will be returned if the resource did not exist.
DELETE /{path}/{model}/{id}
Issuing a delete request to this route will result in the deletion of the resource and a 204
response if successful. If there was no resource, a 404
will be returned.
Mongoose sub-documents let you define schemas inside schemas, allowing for nested data structures that can be validated and hooked into via middleware.
var commentSchema = new Schema({
message: { type: String, required: true }
});
var postSchema = new Schema({
title: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: 'User', index: true },
comments: [ commentSchema ],
createdAt: Date
});
mongoose.model('Post', postSchema);
Meanify will detect sub-documents and expose SCRUD routes beneath the top-level models.
meanify GET /api/posts/:id/comments +1ms
meanify POST /api/posts/:id/comments +0ms
meanify GET /api/posts/:id/comments/:commentsId +0ms
meanify POST /api/posts/:id/comments/:commentsId +0ms
meanify DELETE /api/posts/:id/comments/:commentsId +0ms
These routes work similar to the model SCRUD routes defined above, with the exception of the Search route. Currently, options are not supported so a GET
to the sub-document collection will simply return the entire array set.
The sub-document middleware is made available underneath the parent middleware.
app.post('/api/posts/:id/comments/:commentsId', meanify.posts.comments.update);
Meanify will handle validation described by your models, as well as any error handling defined in your pre
middleware hooks.
postSchema.path('type').validate(function (value) {
return /article|review/i.test(value);
}, 'InvalidType');
The above custom validator example will return a validation error if a value other than "article" or "review" exists in the type
field upon creation or update.
Example response:
{
message: 'Validation failed',
name: 'ValidationError',
errors: {
type: {
message: 'InvalidType',
name: 'ValidatorError',
path: 'type',
type: 'user defined',
value: 'poop'
}
}
}
}
Advanced validation for the create and update routes may be achieved using the pre
hook, for example:
commentSchema.pre('save', function (next) {
if (this.message.length <= 5) {
var error = new Error();
error.name = 'ValidateLength'
error.message = 'Comments must be longer than 5 characters.';
return next(error);
}
next();
});
Meanify will return a 400
with the error object passed by your middleware.
{
name: 'ValidateLength',
message: 'Comments must be longer than 5 characters.'
}
Using hooks, you can access Express' request
, response
objects and next
function for maximum control over the response to the client. This is especially useful for authorization.
Hooks can be registered via options
and passed into meanify on initialization. For example:
// Options Registration
var meanify = require('../meanify')({
path: '/api',
hooks: {
// Include the Schema name registered in Mongoose.
Post: {
// Key should be `search`, `create`,
// `read`, `update`, or `delete`
read: function (req, res, done, next) {
// Do some stuff.
done();
},
delete: function (req, res, done, next) {
// Do different stuff.
done();
}
}
}
});
app.use(meanify());
Alternatively, you can add hooks via the middleware object created after initialization. For example:
// Dynamic Registration
meanify.posts.hook('create', function (req, res, done, next) {
var doc = this;
if (doc.user === req.user._id) {
// Proceed as normal.
done();
} else if (badDog) {
res.status(401).send({
name: 'Unauthorized',
message: 'You can`t do that, Dave.'
});
} else if (goodDog) {
// Tell `meanify` to send a `400` response with this JSON body.
done({
name: 'HookError',
message: 'This is a completely custom error!'
});
} else {
// Bypass `meanify` and generate a `500` for Express to handle.
// 500 error handled by Express
next(err);
}
});
Meanify exposes hooks for the search
, create
, read
, update
and delete
operations on primary resources. Sub-documents are not currently supported.
The search
hook executes after the results have been retrieved from the database, just before sending to the client. This is useful for manipulating search results or tailoring them to users. this
refers to the search results.
The create
hook takes place immediately after receiving the request, right before Mongoose attempts validation. this
refers to the newly initialized Mongoose object (i.e. Model.create(req.body)
).
The read
hook fires after Mongoose successfully finds the resource, just before returning the data to the client. this
refers to the retrieved Mongoose resource.
The update
hook fires after Mongoose successfully finds the resource to update, right before it attempts validation to save it. this
refers to the retrieved Mongoose resource with updated properties.
The delete
hook executes after meanify successfully locates the resource to delete, right before attempting to remove the document from the collection. this
refers to the retrieved Mongoose resource to delete.
Hook callbacks are invoked with the req
, res
, done
and next
arguments:
req
- The Expressrequest
object.res
- The Expressresponse
object.done
- The Meanify callback. Sending with an object generates a400
HTTP error and returns the object as JSON to the client.next
- The Expressnext
function. Callingnext()
skips to subsequent middleware or404
handler. Callingnext(err)
invokes Express'500
error handler.
Instance methods can also be defined on models in Mongoose and accessed via the meanify API routes.
postSchema.method('mymethod', function (req, res, done) {
var error = null;
var query = req.query;
var body = req.body;
if (query.foo) {
// Do some stuff with query params.
body.foo = query.foo;
// Return new object in the response.
done(error, body);
} else {
error = {
name: 'NoFoo',
message: 'Foo not found.'
};
// Send error.
done(error);
}
});
Instance methods defined on schemas should expect three arguments: the request
object, response
object, and a done
callback.
If the done
callback sees null
as the first argument and the modified resource object second, a successful 200
response will be returned along with the resource object as the body. If the callback sees the error defined as the first argument, a failure (i.e. a 400
) response is returned.
The response
object gives you more control over the response and status code sent. For example, you could send a 401
or 403
response status (i.e. res.status(401).send()
) instead of the standard 400
using the callback.
Note that inside the schema method context, this
refers to the resource document found in the database.
Note: When naming your instance methods, choose something other than the built-in instance methods in Mongoose.
Methods are invoked by adding it after the id
segment of the POST
request.
POST /{path}/{model}/{id}/{method}?foo=bar
The query parameters and body of the POST
are passed to the instance method, returning a 200
status and resource body if successful, or a 400
status and error object if not.
Custom instance methods are made available under the meanify.update
property for greater control in your routes. Note that the :id
parameter is required.
app.post('/api/posts/:id/mymethod', meanify.posts.update.mymethod);
Note: Instance methods are not supported on sub-documents.
- Nested sub-documents.
- Sub-document instance methods.
- Static methods.
- Generation of AngularJS ngResource service via
/api/?ngResource
endpoint. - Examples and documentation on integration in AngularJS.
- Hooks for
search
,create
,read
,update
, anddelete
operations.
- Support for Mongo's distinct aggregation command via the
__distinct
option.
- Instance method constructor supports
req
,res
,next
interface.
- Generated routes and middleware for schema sub-documents.
- Validation and error handling in middleware
pre
hooks.
- Generated routes and middleware for model instance methods.
exclude
option excludes models from router but retains middleware functions.
- JSON object support in query parameters, enabling range queries.
body-parser
middleware is bundled in meanify router.- Started unit test framework and added
.jshintrc
.
- Basic example of a service using meanify.
- Alpha release ready for publish to npm and testing.