# Controllers & API Routing

Controllers are automatically detected and loaded when placed in a feature's api/controllers folder. This happens as part of #lib/InitApi->getControllers() which is run when initializing features on API startup. It is recommended that you organize your controllers content according to routes/resources for your API, but ultimately, you can set up any routing scheme you want.

The simplest example of a controller looks like:

const { PrivateController } = require('#api/Controllers/PrivateController');

module.exports = class SampleController extends PrivateController {
  /**
   * Constructor.
   *
   * @param {MassiveJS} db - The MassiveJS db connection.
   * @param {Mailer} mailer - The mailer.
   */
  constructor({ db, mailer } = {}) {
    super({ db, mailer });

    this.router.get('/hello', this.sayHi.bind(this));
  }

  async sayHi(req, res) {
    res.json({ data: { message: 'hi' } });
  }
};

When /api/hello is visited, authenticated users will be returned a welcoming JSON message.

# Types of Controllers

Currently, there are three types of controllers, but you are free to create more for any specific circumstances that your app may require.

# Private Controllers

Private Controllers will prepend their routes with /api and will require users to be logged in to access their methods. Any attempt to access without providing the JWT of a logged-in user will return a 401 HTTP Response, which will redirect the user to the login form.

To create a Private Controller, use code similar to the following:

const { PrivateController } = require('#api/Controllers/PrivateController');

module.exports = class SamplePrivateController extends PrivateController { }

# Admin Controllers

Admin Controllers are similar to Private Controllers, except that they prepend their routes with /api/admin and require that all users accessing their methods have the accountType of admin, or else be returned a 401 HTTP Response. This ensures that only users you specify can access your Admin Panel.

To create an Admin Controller, use code similar to the following:

const { AdminController } = require('#api/Controllers/AdminController');

module.exports = class SampleAdminController extends AdminController { }

# Public Controllers

Public Controllers prepend their routes with /api/public and allow anyone to access their methods, not just logged-in users.

To create a Public Controller, use code similar to the following:

const { PublicController } = require('#api/Controllers/PublicController');

module.exports = class SamplePublicController extends PublicController { }

# Routing

Routes are defined in a controller's constructor function. Every controller has a router object on which routes are applied, which ensures that all routes are prepended appropriately. For example, the route /users/me will on a Private Controller actually be available at /api/users/me, and on an Admin Controller, would be available at /api/admin/users/me.

Aside from that, Nodewood's routing is just Express's routing, so anything that works there works here. For example, to set up a POST route for creating a comment that has a middleware ensuring the user's account is in good standing:

this.router.post(
  '/comments',
  this.ensureUserNotBanned,
  this.reply.bind(this),
);

reply is a controller class method that is just an Express route method. That means it has a req Request object passed in that you can examine to determine the parameters of the request, and a res Response object passed in that you can use to send a response to the request. For more information on this topic, you should consult the Express docs, as they are excellent.

You may notice that the reply class method has .bind(this) attached to it. This is so that the scope of the object inside the controller method is correct and when you use this, it points to the controller and not the router. The ensureUserNotBanned middleware in this example doesn't refer to this and thus doesn't need to be specially bound.

# Authentication and Public routes

Any controller that extends a PublicController will have its route prepended with /api/public and is considered a "public" route that does not require a valid JWT to access. This is useful for account creation, user login, and other routes where you wouldn't or couldn't have a valid JWT yet.

Routes on Private Controllers and Admin Controllers will have their JWT cookie checked before the user is allowed access. A number of things have to match in order for a JWT to be considered valid:

  • The user's hashid (so your user count cannot be easily enumerated) is decoded and we attempt to load a user with that ID.
  • The user's JWT Series number is checked against the one stored in the database. If the user changes their password, this number is incremented, rendering all JWTs invalid and forcing a login on all previously-validated devices.
  • The global JWT series is checked against the one stored in the .env config. This allows you to invalidate literally ever JWT for every user, in case of a serious security breach.
  • The token's expiry date is checked, ensuring tokens older than what is set in the security config are invalid.
  • Finally, a CSRF token that is appended to the headers of every request is checked to ensure that we're not inadvertently allowing cross-site request forgeries.

If a JWT is not considered valid, a Standard401Error is thrown and a 401 HTTP response is returned. The UI is smart enough to detect 401 errors and redirect users to /app/login to attempt to sign in again.

# Adding a new controller

To add a new controller with some sample code to get you rolling, run:

nodewood add controller FEATURE NAME

Replace FEATURE with the name of the feature you wish to add a controller for, and NAME with the name of the controller you wish to create. By default, the created controller is a Private Controller, but you can change to another kind simply by changing what it extends.

# Tests

Controller tests should be located in a feature's api/controllers/__tests__ folder. Keeping tests near their controller makes them easier to navigate to, in the same way that keeping frequently-used items in your kitchen makes it easier to work. Nodewood uses Jest for testing, and if you use the Nodeood CLI tool to scaffold your feature or controller, it comes with a sample test all set up for you to add to.

To run your tests, run yarn test from the root folder of your project.