Skip to content

About

Restify-Mongoose provides a resource abstraction to expose mongoose models as REST resources.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

110 stars

Watchers

7 watching

Forks

Restify-Mongoose

NPM

CI npm version npm downloads License

Restify-Mongoose provides a resource abstraction for restify to expose mongoose models as REST resources.

Getting started

First you'll need to install restify-mongoose via npm

npm install restify-mongoose

Second step is to wire up mongoose and restify using restify-mongoose

import restify from 'restify';
import restifyMongoose from 'restify-mongoose';
import mongoose from 'mongoose';

const server = restify.createServer({
  name: 'restify.mongoose.examples.notes',
  version: '1.0.0'
});

server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.queryParser());
server.use(restify.plugins.bodyParser());

// Define Mongoose schema & model for 'Note'
interface INote {
  title: string;
  date: Date;
  tags?: string[];
  content?: string;
}

const NoteSchema = new mongoose.Schema<INote>({
  title: { type: String, required: true },
  date: { type: Date, required: true },
  tags: [String],
  content: { type: String }
});

const Note = mongoose.model<INote>('notes', NoteSchema);

// Create a restify-mongoose resource from the 'Note' model
const notes = restifyMongoose(Note);

// Serve resource notes with fine-grained route mapping
server.get('/notes', notes.query());
server.get('/notes/:id', notes.detail());
server.post('/notes', notes.insert());
server.patch('/notes/:id', notes.update());
server.del('/notes/:id', notes.remove());

server.listen(3000, () => {
  console.log(`${server.name} listening at ${server.url}`);
});

Resources

To map resources or resource functionality to restify REST endpoints/routes restify-mongoose offers two approaches:

  • 'fine grained mapping' control via list, detail, new, update and delete functions
  • 'quick mapping' via serve method

Fine grained mapping In the above getting started example we used fine grained mapping control. Restify-mongoose defines the functions query, detail, insert, update and remove that return restify route handlers and can be used like this:

 // Serve resource notes with fine grained mapping control
 server.get('/notes', notes.query());
 server.get('/notes/:id', notes.detail());
 server.post('/notes', notes.insert());
 server.patch('/notes/:id', notes.update());
 server.del('/notes/:id', notes.remove());

For every ´id´ dependent function the restify route has to define a :id placeholder to allow restify-mongoose to access id parameters. Id dependent functions are detail, update and delete.

Query String

Setting a queryString will make restify-mongoose use the string as the field name to conduct its searches in the detail update & remove functions. If not set it will use the default behavior of using mongos _id field.

// Create a restify-mongoose resource with a custom lookup field
const notes = restifyMongoose(Note, { queryString: 'slug' });

// Routes will now look up documents by 'slug' instead of '_id'
server.get('/notes/:id', notes.detail());
server.patch('/notes/:id', notes.update());
server.del('/notes/:id', notes.remove());

Quick mapping

// Serve resource notes with quick mapping
const notes = restifyMongoose(Note);
notes.serve('/api/notes', server);

Automatically mounts:

  • GET '/api/notes' -> query()
  • GET '/api/notes/:id' -> detail()
  • POST '/api/notes' -> insert()
  • DELETE '/api/notes/:id' -> remove()
  • PATCH '/api/notes/:id' -> update()

You can also pass an options object to the serve method to attach handlers before and after the request (e.g. authentication middleware):

// Serve resource notes with quick mapping and auth middleware
notes.serve('/api/notes', server, {
  before: [jwt({ secret: 'some-secret' })]
});

Queries

Query parameters are passed by query string parameter q.

Query parameters are parsed as JSON objects and passed to mongoose where query function.

To filter a notes resource by title to match term "first" append the q query parameter to the URL:

http://localhost:3000/notes?q={"title":"first"}

Query Security & Sanitization

By default, restify-mongoose operates on a whitelist-first security model to protect against NoSQL injection:

  • Object & Prototype Safety: Queries must parse into a valid JSON object. Prototype pollution keys (__proto__, constructor, prototype) and non-object payloads are rejected with HTTP 400.
  • Default Operator Whitelist: Only safe, standard query operators are permitted by default:
    • Comparison: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin
    • Logical: $and, $or, $not, $nor
    • Element: $exists, $type
    • Array: $all, $elemMatch, $size
    • Evaluation: $regex, $options
  • Default Deny: Any operator not in the whitelist (e.g. $where, $expr, $function, $accumulator, or unknown operators) is rejected with HTTP 400.

Customizing Allowed Operators (queryOperators)

You can customize which query operators are permitted at the resource or query route level:

  • Default: Standard whitelist of safe comparison, logical, and array operators.
  • string[]: Custom whitelist specifying exactly which operators are allowed.
  • 'none' or false: Strict mode — disallows all $ operators (allowing only direct key-value matching).
  • 'all' or true: Unrestricted mode — permits any MongoDB query operator.
import restifyMongoose, { DEFAULT_ALLOWED_OPERATORS } from 'restify-mongoose';

// Strict mode: disallow all MongoDB operators
const notes = restifyMongoose(Note, { queryOperators: 'none' });

// Custom whitelist: only allow specific operators
const notes = restifyMongoose(Note, {
  queryOperators: ['$gt', '$gte', '$lt', '$lte', '$in']
});

// Extend default whitelist with additional operators
const notes = restifyMongoose(Note, {
  queryOperators: [...DEFAULT_ALLOWED_OPERATORS, '$text']
});

// Or configure per-route
server.get('/notes', notes.query({ queryOperators: false }));

Restricting Queryable Fields (queryFields)

To prevent clients from querying internal or sensitive fields, specify an allowed fields list as an array or comma-separated string:

// Only allow queries on title and date
const notes = restifyMongoose(Note, {
  queryFields: ['title', 'date']
});

// Or as a comma-separated string
server.get('/notes', notes.query({ queryFields: 'title,date' }));

Paginate

Requests that return multiple items in query will be paginated to 100 items by default. You can set the pageSize (number min=1) by adding it to the options.

const notes = restifyMongoose(Note, {
  pageSize: 2
});

or as query string parameter pageSize (which will have the presedence)

http://localhost:3000/notes?pageSize=2

You can specify further pages with the p parameter and a page number.

http://localhost:3000/notes?p=1

An additional restriction to page sizes can be made with maxPageSize option (default value is 100) that defines the maximum allowed page size to avoid unbound queries.

Link Header

The pagination info is included in the Link header. It is important to follow these Link header values instead of constructing your own URLs.

link:
<http://example.com/notes?p=0>; rel="first",
<http://example.com/notes?p=1>; rel="prev",
<http://example.com/notes/?p=3>; rel="next",
<http://example.com/notes/?p=4>; rel="last"

Linebreak is included for readability.

You can set the baseUrl by adding it to the options.

const notes = restifyMongoose(Note, {
  baseUrl: 'http://example.com'
});

The possible rel values are:

  • next - Shows the URL of the immediate next page of results.
  • last - Shows the URL of the last page of results.
  • first - Shows the URL of the first page of results.
  • prev - Shows the URL of the immediate previous page of results.

Total Count Header

The total number of results/resources returned in query is sent in the X-Total-Count Header and is not affected by pagination (setting pageSize and p parameter). It does take in account filter and query parameter ( q ).

Sort

Sort parameters are passed by query string parameter sort.

Sort parameters can be separated by comma or space. They will be passed directly to mongoose sort query function.

To sort a notes resource by title descending append the sort query parameter to the URL:

http://localhost:3000/notes?sort=-title

You can also define a default sort in the options object. This option will by ignored if a sort query parameter exists.

Using in the constructor:

const notes = restifyMongoose(Note, { sort: '-title' });
notes.serve('/notes', server);

Using per-route:

const notes = restifyMongoose(Note);
server.get('/notes', notes.query({ sort: '-title' }));

## Select Fields
To restrict selected columns you can pass a query string parameter __select__.

Select fields can be separated by comma or space. They will be passed to [mongoose select query function](http://mongoosejs.com/docs/api.html#query_Query-select).

To select only title and date the fields of a notes resource append the __select__ query parameter to the URL:

    http://localhost:3000/notes?select=title,date

You can also define select fields in the options object. This will make the the __select__ query parameter be ignored.

Using in the constructor:
```typescript
const notes = restifyMongoose(Note, { select: 'title' });
notes.serve('/notes', server);

Using per-route:

const notes = restifyMongoose(Note);
server.get('/notes/:id', notes.detail({ select: 'title,date,tags' }));
server.get('/notes', notes.query({ select: 'title date' }));

Filter

Results can be filtered with a function, which is set in the options object of the constructor or per-route on query, detail, update, and remove.

The function takes two parameters: the request object and the response object. The return value is a query object passed directly to the mongoose where query function.

Filter functions support both synchronous returns and Promises/async functions:

// Synchronous filter
const filterUser = (req, res) => {
  return { user: req.user.id };
};

const notes = restifyMongoose(Note, { filter: filterUser });

// Asynchronous filter (e.g. checking permissions or external service)
const notes = restifyMongoose(Note, {
  filter: async (req, res) => {
    const orgId = await fetchUserOrganization(req.user.id);
    return { organization: orgId };
  }
});

// Route-level filter (overrides Resource-level filter)
server.get('/notes', notes.query({
  filter: (req, res) => ({ public: true })
}));
server.del('/notes/:id', notes.remove({
  filter: (req, res) => ({ user: req.user.id }) // users can only delete their own notes
}));

Projection

A projection transforms a document before sending it in the response for query and detail operations. This is useful for omitting sensitive fields (e.g. passwordHash) or transforming document structure.

Projections support both modern async functions and classic callbacks:

// Async projection (Recommended)
const userProjection = async (req, item) => {
  return {
    id: item._id,
    username: item.username,
    email: item.email
  };
};

// Callback projection
const userProjectionCallback = (req, item, cb) => {
  cb(null, {
    id: item._id,
    username: item.username,
    email: item.email
  });
};

Projections can be configured on the Resource constructor (listProjection and detailProjection) or per-route on query and detail:

// On the constructor
const users = restifyMongoose(User, {
  listProjection: userProjection,
  detailProjection: userProjection
});
users.serve('/users', server);

// Per-route
const users = restifyMongoose(User);
server.get('/users/:id', users.detail({ projection: userProjection }));
server.get('/users', users.query({ projection: userProjection }));

beforeSave

The beforeSave hook allows inspecting or transforming a document before it is saved to MongoDB during insert (POST) and update (PATCH) operations. If validation or authorization fails, an error can be returned or thrown to abort the save.

beforeSave can be defined in the Resource constructor options or passed directly to insert({ beforeSave }) or update({ beforeSave }).

Both async/await and callback forms are supported:

// Async / Promise style (Recommended)
const notes = restifyMongoose(Note, {
  beforeSave: async (req, item) => {
    // Modify model before persisting
    item.updatedBy = req.user.id;
    item.lastModified = new Date();

    if (item.isArchived && !req.user.isAdmin) {
      throw new Error('Only administrators can archive notes');
    }
  }
});

// Callback style
const notes = restifyMongoose(Note, {
  beforeSave: (req, item, cb) => {
    item.updatedBy = req.user.id;
    cb(null); // Pass error to cb(err) to abort
  }
});

Authentication and Access Control

restify-mongoose works seamlessly with standard Restify middleware for authentication and multi-tenant access control.

1. Route Authentication Middleware (before)

Use the before option on serve() to enforce authentication (e.g. JWT verification, session checks) across all endpoints:

import restify from 'restify';
import restifyMongoose from 'restify-mongoose';
import Note from './models/note';

const server = restify.createServer();
const notes = restifyMongoose(Note);

// Middleware that authenticates user and sets req.user
function authenticate(req, res, next) {
  const token = req.header('Authorization');
  if (!token) {
    return next(new restify.errors.UnauthorizedError('Authentication required'));
  }
  req.user = verifyToken(token);
  next();
}

// Pass middleware in "before" option
notes.serve('/notes', server, {
  before: [authenticate]
});

2. Tenant and Resource Ownership Isolation (filter)

Combine authentication with dynamic filter (sync or async) to ensure users can only view, update, or delete records they own:

const notes = restifyMongoose(Note, {
  // Enforce ownership across query, detail, update, and remove
  filter: (req, res) => {
    if (req.user.isAdmin) {
      return {}; // Admins can access all records
    }
    return { owner: req.user.id }; // Regular users only see their own
  }
});

notes.serve('/notes', server, {
  before: authenticate
});

3. Creation Ownership & Validation (beforeSave)

Assign ownership when documents are created or updated:

const notes = restifyMongoose(Note, {
  beforeSave: (req, item) => {
    if (req.method === 'POST') {
      item.owner = req.user.id;
    }
  }
});

Output format

The output format can be changed to json-api format to use with JSON:API clients (such as Ember Data).

When outputFormat: 'json-api' is enabled:

  • Responses automatically include the standard Content-Type: application/vnd.api+json header.
  • Responses are formatted with the model name as the root key: { [modelName]: data }.
  • Supported across all endpoints: query, detail, insert, update, and remove.
const users = restifyMongoose(User, { outputFormat: 'json-api' });
users.serve('/users', restifyServer);

You can also customize the root key with modelName:

const users = restifyMongoose(User, {
  outputFormat: 'json-api',
  modelName: 'admins'
});
users.serve('/users', restifyServer);

Populating referenced documents

The returned results can use mongoose's "populate" query modifier to populated referenced documents within models.

Referenced documents can be populated in three ways:

query parameter

Adding populate=[referenced_field] to the query string will populate the referenced_field, if it exists.

Resource option

const notes = restifyMongoose(Note, { populate: 'author' });

query / detail method options

server.get('/notes', notes.query({ populate: 'author' }));
server.get('/notes/:id', notes.detail({ populate: 'author' }));

Populating multiple fields

Multiple referenced documents can be populated by using a comma-delimited list of the desired fields:

const notes = restifyMongoose(Note, { populate: 'author,contributors' });

Advanced Populate Options (Objects & Arrays)

In addition to string lists, populate accepts standard Mongoose populate options objects or arrays of objects (e.g. for selective field population or deep population):

// Object syntax with path and select
const notes = restifyMongoose(Note, {
  populate: { path: 'author', select: 'name email' }
});

// Array of populate options
server.get('/notes', notes.query({
  populate: [
    { path: 'author', select: 'name' },
    { path: 'contributors', select: 'name avatar' }
  ]
}));

Contribute

Contribution welcome! Read the contribution guideline first.

About

Restify-Mongoose provides a resource abstraction to expose mongoose models as REST resources.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

110 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages