A flexible REST backend with an ORM like frontend for Mongoose, working well in React.
the.rest sets up all REST routes automatically and corresponding frontend classes that consume them but looks like normal classes to you.
This module is meant to be used together with Express and Mongoose so if you haven't done so already:
npm install express
npm install mongoose
Then:
npm install the.rest
Here is an example of fairly typical backend setup
// Modules
const path = require('path');
const express = require('express');
const mongoose = require('mongoose');
const theRest = require('the.rest');
// Connect to MongoDB via Mongoose
mongoose.connect('mongodb://localhost/db-name', {
useNewUrlParser: true,
useUnifiedTopology: true
});
const db = mongoose.connection;
// Create an Express server
const app = express();
// ..and install the.rest as middleware
// Arguments/configuration:
// 1) The express library
// 2) The base route for the REST api to create
// 3) The path to a folder with mongoose-models
// Please Note: This path must be absolute
const pathToModelFolder = path.join(__dirname, 'mongoose-models');
app.use(theRest(express, '/api', pathToModelFolder));
// Add other middleware you might need (express.static etc)
// Listen on port 5000
app.listen(5000, () => console.log('Listening on port 5000'));
Create a folder named mongoose-models and a file for each mongoose model. Each file should export a mongoose model. It should look something like this:
const mongoose = require('mongoose');
const modelName = 'Cat';
const schema = {
name: String
};
module.exports = mongoose.model(modelName, schema);
React: Proxying API Requests in development
Vue: Devserver proxy
If you are using plaing old JavaScript (no import-statements/build environments) you can include the frontend part of the.rest like this:
<script src="/REST.js">
You will now have a global object called REST. Each Mongoose.model will be available as a property - so if you have created a model called Cat and a model called Dog, their frontend equivalents would be available as REST.Cat, REST.Dog etc.
If you want individual variables just use a destructuring assignment:
const {Cat, Dog} = REST;
If you are in an environment where you import dependencies using import do:
import REST from 'the.rest/dist/to-import';
Each Mongoose.model will be available as a property - so if you have created a model called Cat and a model called Dog, their frontend equivalents would be available as REST.Cat, REST.Dog etc.
You can also use named imports:
import {Cat, Dog} from 'the.rest/dist/to-import';
the.rest makes it really easy to find, save and delete Mongoose instance from your frontend code.
The API is optimized to be used together with await (inside async functions).
Creating and saving a new instance is simple:
// Create a new cat
let g = new Cat({name: 'Garfield'});
// save it to the db (the properrty _id is also added)
await g.save();
Find all Cats:
let c = await Cat.find()
let c = await Cat.findOne({_id: '5d793d86d3af842b4f92121c'});
Shorthand for find by id is:
let theCat = await Cat.findOne('5d793d86d3af842b4f92121c');
Any query you can use with Mongo/Mongoose can be used:
// Fin all cats starting with 'gar' in their name
let cats = await Cat.find({name:/gar/i});
Mongoose has a lot of extra methods for controlling queries (select, sort, limit, populate etc).
Most of them will work from the frontend with the.rest. You can use the same syntax as in normal Mongoose (writing method chains), execept that you leave out the exec call.
A typical example of Mongoose syntax (backend):
await Cat.find({}).sort('name').limit(10).select('name').exec();
The same thing witten in the.rest syntax on the frontend:
await Cat.find({}).sort('name').limit(10).select('name');
If you are targeting old browsers/Internet Explorer, that do not support the Proxy object, you have to use an alternate syntax, where you replace the method chains with a second argument:
await Cat.find({}, {sort: 'name', limit: 10, select: 'name'});
You use save for updates as well as for creation.
let theCat = await Cat.findOne('5d793d86d3af842b4f92121c');
theCat.name += ' Supercat';
await theCat.save();
let theExCat = await Cat.findOne('5d793d86d3af842b4f92121c');
await theCat.delete();
The.rest uses a special array subclass that have the methods save and delete - and lets you save/update/delete several instances at once.
// Create the cats
let cats = new Cat.array(
new Cat({name: 'A'}),
new Cat({name: 'B'}),
new Cat({name: 'C'})
);
// Save them all at once
await cats.save();
// Find the cats
let cats = Cats.find();
// Update their names
cats.forEach(x => x.name += ' The Greatest');
// Save them all at once
await cats.save();
// Find the cats
let cats = Cats.find({name: /gar/i});
// Delete them all at once
await cats.delete();
If you want a dog who can bark and sit you simply add the methods to the Dog class like this:
Object.assign(Dog.prototype, {
bark(){
return `Woof! I am ${this.name}!`;
},
sit(){
return `I, ${this.name}, am sitting. Give me candy!`;
}
});
Population with Mongoose is the equivalent of joins in SQL. When you use the.rest you can add the parameter populateRevive to revive populated fields as real the.rest frontend classes.
Given that we have the models Elephant and Tiger and favoriteTiger: {type: mongoose.Schema.Types.ObjectId, ref: 'Tiger'} in the Elephant schema:
// Delete all tigers and elephants
await (await Tiger.find()).delete();
await (await Elephant.find()).delete();
// Create a tiger
let tigger = new Tiger({ name: 'Tigger' });
await tigger.save();
console.log('tigger', tigger);
// Create an elephant that likes Tigger
let dumbo = new Elephant({ name: 'Dumbo', favoriteTiger: tigger});
await dumbo.save();
console.log('dumbo', dumbo);
// All elephants populated with tigers
let elephants = await Elephant
.find({})
.populate('favoriteTiger')
.populateRevive(Tiger);
console.log('elephants', elephants);
Note: If you want to populate several fields, then call populate with a a space delimited string and populateRevive with an array of classes.
If you want to protect certain routes/actions, based on user priviliges or other considerations you can do so by providing a fourth parameter - a function - to the.rest when you setup your backend.
// See "Backend setup" above for details about basic setup
// My ACL function
async function acl(info, req{
if(info.modelName === 'Elephant' && info.extras.populate){
return 'It is not allowed to populate Elephants.';
}
// Note:
// If you are using npm express-session you can write rules based on
// req.session and storing the logged in user in req.session.user
}
// Note the use of acl as a fourth parameter
// when registrering the.rest as middleware
const pathToModelFolder = path.join(__dirname, 'mongoose-models');
app.use(theRest(express, '/api', pathToModelFolder, acl));
The acl function recieves an info object and the Express request object. It will be called for each request.
Note: If you choose to return something (preferably a string) it means you are not letting the request through.
You can combine this with modules such as express-session to read what user and user priviliges apply from req.session, but in the example above we simply do not allow population of Elephants regardless of user.
The info object has the following structure:
{
route: 'elephants',
requestMethod: 'GET',
modelName: 'Elephant',
model: Model { Elephant },
query: { name: /Dum/i },
extras: { populate: 'favoriteTiger' }
}
You get the base/entity route, the request method, the name of the mongoose model, the actual mongoose model object, the query and the extras (i.e. sort, limit, select, populate etc).
This means you don't have to parse this yourself from req.url
Acl is "invisible"/transparent by default on the frontend - you simply get empty answers when acl kicks in. But if you want to you can register a listener to pick up the acl messages from the backend:
Elephant.acl = message => {
console.warn(message);
};
To unregister:
delete Elephant.acl;
Note: The listener is just a property containing a function. If you want to be able to register several listeners to the same class, build your own event registration system based on this fact.
Sometimes you need to write special routes yourself on the backend, that are not based on Mongoose-models. A common example would be routes for login (- could be done asm POST /api/login logs in, DELETE /api/login logs out and GET /api/login checks who is logged in?).
So in the backend you would write these routes by hand, as opposed to automatically created REST routes from the.rest. However you might still want to use a the.rest based class on the frontend.
You can send a fifth parameter to the rest to do this! It should be an object where the keys are routes and their values are the name of the 'model'/class you want to be created on the front-end.
app.use(theRest(express, '/api', pathToModelFolder, null, {
'login': 'Login'
}));
- 1.0.0 - 1.0.7 Early additions and bug fixes
- 1.0.8 - PopulateRevive was introduced
- 1.0.9 - Acl added and the RESTClientArray class subclassed for each entity.
- 1.0.10 - 10.0.12 - Minor changes to README.
- 1.0.13 - Explanation of the acl info object added to README.
- 1.0.14 - Minor changes to README.
- 1.0.15 - Not sending res to acl anymore
- 1.0.16 - Minor changes to README.
- 1.0.17 - Getting rid of Express as a dependency (now a first argument to middleware conf)
- 1.0.18 - 10.0.19 - Fixing bug/typo that made 1.0.17 unusable
- 1.0.20 - 10.0.22 - Minor changes to README.
- 1.0.23 - Reviving regexps on backend without $regex wrapper property
- 1.0.24-1.0.26 - Introducing method chain syntax
- 1.0.27 - Temporarily reverting method chain syntax
- 1.0.28 - Reintroducing the method chain syntax
- 1.0.29 - Minor changes to README.
- 1.0.30 - Added possibility to add frontend classes that do not have Mongoose/the.rest backend.
- 1.0.31 - Fixed a bug in DELETE routes