Installation

npm i @smallhillcz/routesjs

Dependencies: mongo-parse

Peer dependencies: express

Motivation

As far as I can say there is no NodeJS framework that would support:

Examples

Usage

Import Routes and bind to Express app

routes.js

const { Routes } = require("@smallhillcz/routesjs");
const routes = new Routes();

// your routes
routes.get("posts","/posts").handle( (req,res,next) => { ... } );
routes.get("posts","/posts/comments").handle( (req,res,next) => { ... } );

module.exports = routes;

app.js

// load express app
const express = require("express");
const app = express();

// bind Routes
app.use("/", require("./routes");

app.listen(...) // create server as per express documentation

Making a child router

child.js

const { Routes } = require("@smallhillcz/routesjs");
const routes = new Routes();

// your child routes
routes.get("posts","/").handle(...);
routes.post("posts","/").handle(...);

module.exports = routes;

main.js

const { Routes } = require("@smallhillcz/routesjs");
const routes = new Routes();

routes.child("/posts",require("./child"));

Binding Routes child to Express router

const router = express.Router();

router.use("/posts", require("./child").router);

Binding Routes child to Express app

const app = express();

app.use("/posts", require("./child").router);

Using Express router alongside Routes

routes.router.get(...);
routes.router.post(...);
routes.router.use(...);

Routes

Simplest route definition

routes.get("posts","/posts");

Provide name of the route and url.

Handle the route with Express middleware

routes.get("posts","/posts").handle( async (req,res,next) => {
  const posts = await Post.find();
  res.json(posts);
});

Limit route to be listed only under certain docs (uses mongo-parse fot matching)

routes.post("post:publish", "/posts/:post/publish", { query: { status: "draft" } }).handle(async (req,res) => {
  await Post.findOneAndUpdate({ id:req.params.event }, { status: "public" });
  res.sendStatus(200);
});

Create root API endpoint

Either use routes way including permission to read api:

routes.get(null, "/", { permission: "api:read" }).handle((req,res) => {
  res.json({
    name: "My awesome API",
    _links: RoutesLinks.root(req)
  });
});

or use Express router:

routes.router.get("/", (req,res) => {
  res.json({
    name: "My awesome API",
    _links: RoutesLinks.root(req)
  });
});

The output of GET / will look like this:

{
  name: "My awesome API",
  _links: {
    "self": { href: "/", allowed: { GET: true } }, 
    "posts:self": { href: "/posts", allowed: { GET: true, POST: true } }, 
    "post:self": { href: "/posts/:post", templated: true, allowed: { GET: true } }, 
    "post:comments": { href: "/posts/:post/comments", templated: true, allowed: { GET: true } }
  }
}

// define some routes to be added
routes.get("post","/posts/:post");
routes.patch("post","/posts/:post");
routes.get("post:comments","/posts/:post/comments");
routes.action("post:publish","/posts/:post/publish")

// route to list posts
routes.get("posts","/posts").handle( async (req,res,next) => {
  
  // get posts
  const posts = await Post.find().lean(); // mongoose objects cannot be modified, therefore .lean()
  
  // append links
  req.routes.links(posts,"post"); // "post" defines which routes will be used, here staring with "post:"
  
  // return posts to client
  res.json(posts);
});

The output of GET /posts will look like this:

[
 {
  id: 1,
  name: "Post name",
  _links: {
   "self": { href: "/posts/1", allowed: { GET: true, PATCH: true } }, 
   "comments": { href: "/posts/1/comments", allowed: { GET: true } }
  },
  _actions: {
   "publish": { href: "/posts/1", allowed: true }
  },
  ...
]  

RoutesACL

Define user roles and permissions

Define simple allow


const permissions = {
  "posts:list": { admin: true, editor: true, guest: true },
  "posts:edit": { admin: true, editor: true }
};

TIP! Use preset vars for better readability:

const admin = true, editor = true, guest = true;

const permissions = {
    "posts:list": { admin, editor, guest },  
    "posts:list": { admin, editor }
  }
};
const permissions = {
  ...
  "posts:publish": { admin: true, editor: true, assistantEditor: { postType: "unimportant" } }
  ...
};

Define condition or filter based on function of req

const permissions = {
  ...
  "posts:edit": { admin: true, author: req => ({ author: req.user.id }) }
  ...
};

Set up RoutesACL

Routes.setACL({
  // permissions from previous part
  permissions: require("./permissions"),
  // function to get user roles from req
  userRoles: req => req.user ? req.user.roles || [] : [],
  // user role assigned to every request
  defaultRole: "guest",
  
  // log route access to console
  logConsole: true,
  // how the log should look like
  logString: event => `ACL ${event.result ? "OK" : "XX"} | permission: ${event.permission}, user: ${event.req.user ? event.req.user._id : "-"}, roles: ${event.req.user ? event.req.user.roles.join(",") : "-"}, ip: ${event.req.headers['x-forwarded-for'] || event.req.connection.remoteAddress}`
});
routes.post("events", "/events", { permission: "events:list" }).handle(async (req,res) => {
  const events = await Event.find();
  res.json(events);
});

RoutesPluginsMongoose

Plug the plugin to Mongoose

const { RoutesPluginsMongoose } = require("@smallhillcz/routesjs/lib/plugins/mongoose");

mongoose.plugin(RoutesPluginsMongoose);

Filter mongoose docs according to permissions

routes.get("my-events","/my/events").handle( async (req,res,next) => {
  
  const events = await Event.find().filterByPermission("my-events:list", req); // filter only my events
  
  res.json(events);
});

Known limits

It is not possible to guard against doc, instead returns 404

In the following code the constant events is going to be null as if event was not found. It is not possible to distinguish non existent document from the case when document would be found but is not accessible due to permissions.

routes.get("my-event","/my/events/:event").handle( async (req,res,next) => {
  
  // get event from database
  const event = await Event.findOne().filterByPermission("my-events:read", req); // filter only my events
  
  // event not found
  if(!event) return res.sendStatus(404);
  
  // reurn event
  res.json(event);
});