rooseveltframework / roosevelt

🧸 MVC web framework for Node.js designed to make Express easier to use.
Other
68 stars 33 forks source link

Roosevelt MVC web framework

Build Status codecov npm

Roosevelt is a web application development framework based on Express that aims to be the easiest web framework on the Node.js stack to learn and use.

Some notable features:

Teddy Roosevelt's facial hair is a curly brace.

This is documentation for the current version of Roosevelt. If you need API documentation for a previous version of Roosevelt, look here.

Table of contents

Create and run a Roosevelt app

Prerequisites

First you will need to install Node.js. Both the current and LTS version of Node.js are supported. It is recommended that you have both the current and LTS versions of Node.js installed on your system. To do that, it is recommended that you install Node.js using a Node.js version manager like nvm or nvm-windows rather than the official installer, as a version manager will allow you to switch between multiple versions of Node.js easily.

Roosevelt app generator

The Roosevelt app generator is a command line script based on Yeoman that can create a sample Roosevelt app for you.

To use it, simply run the following command:

npx mkroosevelt

Then follow the prompts.

You can also optionally install the app generator to your system if you like so that it doesn't need to be refetched from npm each time you want to create a new app. To do that, first globally install Yeoman and the Yeoman-based Roosevelt app generator:

npm i -g yo generator-roosevelt

Then create a Roosevelt app using the Roosevelt app generator:

yo roosevelt

Then follow the prompts.

After creating your app, cd to your app's directory and:

See available npm scripts for more ways to run the app.

Create a Roosevelt app manually

It is also possible to create a Roosevelt app without using the app generator. This will result in a more minimalist default configuration (e.g. no CSS or JS preprocessors enabled by default).

To do that:

Use Roosevelt as a static site generator

Create a Roosevelt app using one of the methods above, then set the makeBuildArtifacts param to the value of 'staticsOnly' which will allow Roosevelt to create static files but skip the creation of the MVC directories:

  require('roosevelt')({
    makeBuildArtifacts: 'staticsOnly'
  }).init()

You will also need to set viewEngine if you want to render HTML templates into static pages and supply data to the templates:

  require('roosevelt')({
    makeBuildArtifacts: 'staticsOnly',
    viewEngine: 'html: teddy',
    onServerInit: (app) => {
      app.get('htmlModels')['index.html'] = {
        hello: 'world!'
      }
    }
  }).init()

If model data is not supplied by configuration, Roosevelt will try to automatically load a model from a JS file with the same name alongside the template if it exists instead. For example if an index.js file exists next to index.html and the model is not defined by configuration like in the example above, then the index.js file will be used to set the model so long as it exports either an object or a function that returns an object.

Available npm scripts

Roosevelt apps created with the app generator come with the following notable npm scripts prepopulated in package.json:

Available command line arguments

Combining npm scripts and command line arguments

The npm scripts can be combined with the command line flags.

For example, running npm run d -- -r will run your app in development mode and force the HTML validator to be disabled.

Recognized environment variables

The following is a list of environment variables that Roosevelt listens for.

Environment variable precedence:

Default directory structure

Below is the default directory structure for an app created using the Roosevelt app generator:

Configure your app with parameters

Roosevelt is designed to have a minimal amount of boilerplate so you can spend less time focused on configuration and more time writing your app. All parameters are optional. As such, by default, all that's in app.js is this:

require('roosevelt')().startServer()

Roosevelt will determine your app's name by examining "name" in package.json. If none is provided, it will use Roosevelt Express instead.

There are multiple ways to pass a configuration to Roosevelt:

In addition, all parameters support template literal style variable syntax that you can use to refer to other Roosevelt parameters. For example:

{
  "port": 4000,
  "https": {
    "port": "${port + 1}"
  },
  "css": {
    "sourcePath": "css",
    "output": ".build/${css.sourcePath}"
  }
}

Resolves to:

{
  "port": 4000,
  "https": {
    "port": 4001
  },
  "css": {
    "sourcePath": "css",
    "output": ".build/css"
  }
}

MVC parameters

Development mode parameters

Deployment parameters

App behavior parameters

Isomorphic parameters

Statics parameters

Events

Roosevelt provides a series of events you can attach code to by passing a function to the desired event as a parameter to Roosevelt's constructor like so:

require('roosevelt')({
  onServerStart: (app) => { /* do something */ }
})

Event list

Making model files

Place a file named dataModel.js in mvc/models.

Here's a simple example dataModel.js data model:

module.exports = () => {
  return {some: 'data'}
}

In more complex apps, you might query a database to get your data instead.

Making view files

Views by default are Teddy templates. See the Teddy documentation for information about how to write Teddy templates.

You can also use different templating engines by tweaking Roosevelt's MVC parameters.

Making controller files

Controller files are places to write Express routes. A route is the term Express uses for URL endpoints, such as http://yoursite/blog or http://yoursite/about.

Controllers bind models and views together.

To make a new controller, make a new file in the controllers directory. For example:

module.exports = (router, app) => {
  // router is an Express router
  // and app is the Express app created by Roosevelt

  // standard Express route
  router.route('/about').get((req, res) => {

    // load a data model
    let model = require('models/dataModel')()

    // render a template and pass it the model
    res.render('about', model)
  })
}

Sometimes it is also useful to separate controller logic from your routing. This can be done by creating a reusable controller module.

An example would be creating a reusable controller for "404 Not Found" pages:

// reusable controller "notFound.js"
module.exports = (app, req, res) => {
  let model = { content: 'Cannot find this page' }
  res.status(404)
  res.render('404', model)
}

Reusable controller modules differ from standard controller modules in that they accept req and res arguments in addition to app. They are meant to be called from within routes rather than define new routes.

This allows them to be called at will in any other controller's route when needed:

// import the "notFound" controller logic previously defined
const throw404 = require('controllers/notFound')

module.exports = (router, app) => {
  router.route('/whatever').get((req, res) => {

    // test some logic that could fail
    // thus triggering the need for the 404 controller
    if (something) {

      // logic didn't fail
      // so render the page normally
      let model = require('models/dataModel')
      res.render('whatever', model)
    }
    else {

      // logic failed
      // so throw the 404 by executing your reusable controller
      throw404(app, req, res)
    }
  })
}

Making isomorphic controller files

You can also write isomorphic controller files that can be shared on both the client and the server:

// isomorphic controller file about.js
module.exports = (router, app) => {
  router.route('/about').get((req, res) => {
    let model

    // do any pre-render server-side stuff here
    if (router.server) {
      // populate the model with database queries and other transformations that are exclusive to the server
      // isoRequire allows you to require a file only on the server; it will always return false on the client
      // this makes it possible to share this file with frontend module bundlers without server-exclusive files
      // being included in your bundle
      model = router.isoRequire('models/global')(req, res) // get some data common to all pages

      // do things you only need to do if it's a server-side render (when serving HTML from the server, not JSON)
      if (router.serverSideRender(req)) {
        // do SSR-exclusive things here
      }
    }

    // do any pre-render client-side stuff here
    if (router.client) {
      model = window.model // assuming this was fetched from somewhere at some point beforehand
    }

    // do any pre-render stuff common to both the backend and frontend here before calling the render method
    model.content.pageTitle = 'About'

    // if it's an API request (as defined by a request with content-type: 'application/json'), then it will send JSON data
    // if not, it will render HTML
    router.apiRender(req, res, model) || res.render('about', model)

    if (router.client) {
      // do any post-render client-side stuff here (e.g. DOM manipulation)
    }
  })
}

roosevelt-router

When using controller files on the client, you will need to include and configure roosevelt-router in your main JS bundle before loading your controller files:

 // main.js — frontend JS bundle entry point

 // require and configure roosevelt-router
 const router = require('roosevelt/lib/roosevelt-router')({

   // your templating system (required)
   templatingSystem: require('teddy'),

   // your templates (required)
   // requires use of clientViews feature of roosevelt
   templateBundle: require('views'),

   // supply a function to be called immediately when roosevelt-router's constructor is invoked
   // you can leave this undefined if you're using teddy and you don't want to customize the default SPA rendering behavior
   // required if not using teddy, optional if using teddy
   onLoad: null,

   // define a res.render(template, model) function to render your templates
   // you can leave this undefined if you're using teddy and you don't want to customize the default SPA rendering behavior
   // required if not using teddy, optional if using teddy
   renderMethod: null
 })

// load all isomorphic controllers
// leverages isomorphicControllers roosevelt feature
require('controllers')(router)

router.init() // activate router
API

Constructor parameters:

When you call roosevelt-router's constructor, e.g. const router = require('roosevelt/lib/roosevelt-router')(params), the params object can accept the following methods:

Instance members:

When you get a router object after instantiating roosevelt-router e.g. const router = require('roosevelt/lib/roosevelt-router')(params), the following properties and methods are available to you:

Express variables exposed by Roosevelt

Roosevelt supplies several variables to Express that you may find handy. Access them using app.get('variableName').

Express variable Description
express The Express module.
router Instance of router module used by Roosevelt.
routePrefix Prefix appended to routes via the routePrefix param. Will be '' if not set.
routes List of all routes loaded in the Express app by Roosevelt.
viewEngine e.g. teddy by default Any view engine(s) you define will be exposed as an Express variable. For instance, the default view engine is teddy. So by default app.get('teddy') will return the teddy module.
view engine Default view engine file extension, e.g. .html.
formidable The formidable module Roosevelt uses internally. Used for handling multipart forms.
morgan The morgan module Roosevelt uses internally. HTTP request logger middleware.
expressSession The express-session module Roosevelt uses internally. Session middleware.
logger The roosevelt-logger module Roosevelt uses internally. Used for console logging.
modelsPath Full path on the file system to where your app's models folder is located.
viewsPath or views Full path on the file system to where your app's views folder is located.
controllersPath Full path on the file system to where your app's controllers folder is located.
staticsRoot Full path on the file system to where your app's statics folder is located.
publicFolder Full path on the file system to where your app's public folder is located.
htmlPath Full path on the file system to where your app's HTML static page source files are located.
cssPath Full path on the file system to where your app's CSS source files are located.
jsPath Full path on the file system to where your app's JS source files are located.
htmlRenderedOutput Full path on the file system to where your app's rendered and minified staic HTML files are located.
cssCompiledOutput Full path on the file system to where your app's minified CSS files are located.
clientViewsBundledOutput Full path on the file system to where your app's client-exposed views folder is located.
env Either development or production.
params The parameters you sent to Roosevelt.
appDir The directory the main module is in.
appName The name of your app derived from package.json. Uses "Roosevelt Express" if no name is supplied.
appVersion The version number of your app derived from package.json.
package The contents of package.json.
roosevelt:state Application state, e.g. disconnecting if the app is currently being shut down.

Additionally the Roosevelt constructor returns the following object:

Roosevelt constructor returned object members Description
expressApp [Object] The Express app created by Roosevelt.
httpServer [Object] The http server created by Roosevelt. httpServer is also available as a direct child of app, e.g. app.httpServer.
httpsServer [Object] The https server created by Roosevelt. httpsServer is also available as a direct child of app, e.g. app.httpsServer.
reloadHttpServer [Object] The http instance of reload created by Roosevelt.
reloadHttpsServer [Object] The https instance of reload created by Roosevelt.
initServer(callback) [Method] Starts the HTML validator, sets up some middleware, runs the CSS and JS preprocessors, and maps routes, but does not start the HTTP server. Call this method manually first instead of startServer if you need to setup the Express app, but still need to do additional setup before the HTTP server is started. This method is automatically called by startServer once per instance if it has not yet already been called. Takes an optional callback.
init [Method] Shorthand for initServer.
startServer [Method] Calls the listen method of http, https, or both (depending on your configuration) to start the web server with Roosevelt's config.
stopServer(close) [Method] Stops the server and takes an optional argument stopServer('close') which stops the server from accepting new connections before exiting.

Supplying your own CSS preprocessor

In addition to Roosevelt's built-in support for the LESS, Sass, and Stylus preprocessors you can also define your own preprocessor on the fly at start time in Roosevelt's constructor like so:

let app = require('roosevelt')({
  cssCompiler: app => {
    return {
      versionCode: app => {
        // write code to return the version of your app here
      },
      parse: (app, filePath) => {
        // write code to preprocess CSS here
      }
    }
  }
})

API

When a custom preprocessor is defined in this way it will override the selected preprocessor specified in css.compiler.module.

Deploying Roosevelt apps

If you want to deploy a Roosevelt live to the internet, there are some things you should do to harden it appropriately if you expect to take significant traffic.

Use HTTPS

Setting up HTTPS can be tricky to configure properly especially for novices, so it can be tempting not do it to simplify deployment, but your website won't be seen as professional if it isn't served up via HTTPS. It's worth the effort to set it up.

Use a caching service or a database to store sessions

Roosevelt's default session store for express-session works great if your app only needs a single process, but if you're spreading your app across multiple processes or servers, you will need to reconfigure express-session to use a caching service that supports replication like redis or a database that supports replication like PostgreSQL in order to scale your app.

Run the app behind a reverse proxy and use all the CPU cores

To do this, use the production-proxy-mode command line flag and run the process on multiple cores using a tool like pm2.

Then host your app behind a reverse proxy from a web server like Apache or nginx, which is considered a best practice for Node.js deployments.

Running the app in production-proxy mode runs the app in production mode, but with localhostOnly set to true and hostPublic set to false. This mode will make it so your app only listens to requests coming from the proxy server and does not serve anything in the public folder.

You will then need to serve the contents of the public folder directly via Apache or nginx.

Upgrading to new versions of Roosevelt

When you upgrade to a new version of Roosevelt, your Roosevelt config, npm run scripts, or other ways you use Roosevelt may need to be updated to account for breaking changes. There is a config auditor built-in to Roosevelt to detect most such issues, but not everything you might need to change is automatically detected.

Aside from the config auditor, one of the easiest ways to see what you might need to change in your app during a Roosevelt upgrade is to compare changes to the default sample app over time, which you can view here.

Documentation for previous versions of Roosevelt

Contributing to Roosevelt

Here's how to set up a development environment to hack on Roosevelt's code:

Troubleshooting the automated tests

If some of the automated tests fail for you when they shouldn't be, make sure you remove the test/app folder and kill any Node.js processes (e.g. killall node) before running the test suite again.

If you want to see the output from a generated test app in one of the tests, insert this block of code into the test:

testApp.stdout.on('data', (data) => {
  console.log(data.toString())
})

Support Roosevelt's development

You can support Roosevelt's development and maintenance by buying merch or donating. Please note that donations are not tax-deductible. Thank you for your support!