citizen

An MVC-based web application framework. Includes routing, serving, caching, and other helpful tools.

Usage no npm install needed!

<script type="module">
  import citizen from 'https://cdn.skypack.dev/citizen';
</script>

README

citizen 0.9.x

citizen is an MVC-based web application framework designed for people who are more interested in quickly building fast, scalable apps than digging around Node's guts or cobbling together a Jenga tower made out of 20 different packages.

Use citizen as a foundation for a traditional server-side web application, a modular single-page app, or a REST API.

Benefits

  • High performance and stability
  • Zero-configuration server-side routing with SEO-friendly URLs
  • Built-in session management
  • Built-in caching of routes, individual controllers, objects, and static files
  • Directives that make it easy to manage cookies, sessions, redirects, caches, and more
  • Controller-based includes and chaining for powerful code reuse options
  • HTML, JSON, and JSONP served from the same pattern
  • Support for many template engines
  • Few direct dependencies

New for 0.9.x

  • async await and vanilla return statements replace event emitter controller actions
  • Hot module reloading in development mode—no more restarting the app while coding

citizen's API is stabilizing, but it's still subject to change. Always consult the change log before upgrading. Version 0.9.x has significant breaking changes compared to 0.8.x.

Have questions, suggestions, or need help? Send me an e-mail. Want to contribute? Pull requests are welcome.

Want to see citizen in action?

I use it on originaltrilogy.com. We get a moderate amount of traffic (averaging ~400k page views per month and peaking at 750k) and the site runs for months on end without the app/process crashing. It's very stable.

Quick Start

These commands will create a new directory for your web app, install citizen, use its scaffolding utility to create the app's skeleton, and start citizen with the web server listening on port 8080 (citizen defaults to port 80, but it's often in use already, so change it to whatever you want):

$ mkdir mywebapp
$ cd mywebapp
$ npm install citizen
$ node node_modules/citizen/util/scaffold skeleton -n 8080
$ node app/start.js

If everything went well, you'll see confirmation in the console that citizen is listening on the specified port. Go to http://127.0.0.1:8080 in your browser and you'll see a bare index template.

citizen installs Handlebars as its default template engine, but you can install any template engine supported by consolidate.js, update the config template settings, and modify the default view templates accordingly.

For configuration options, see Configuration. For more utilities to help you get started, see Utilities.

App Directory Structure

/app
  /config             // These files are all optional
    citizen.json      // Default config file
    local.json        // Examples of environment configs
    qa.json
    prod.json
  /hooks              // Application event hooks (optional)
    application.js
    request.js
    response.js
    session.js
  /logs               // Log files created by citizen and your app
    app.log
    citizen.log
  /patterns
    /controllers      // Controllers
      index.js
    /models           // Models (optional)
      index.js
    /views
      /error
        error.hbs     // Default error view
      /index
        index.hbs     // Default index view
        other.hbs     // Alternate index view (optional)
  start.js
/web                  // public static assets

Initializing citizen and starting the web server

The start.js file in your app directory can be as simple as this:

// start.js

global.app = require('citizen')

app.start()

Run start.js from the command line:

$ node start.js

Configuration

The config directory is optional and contains configuration files that drive both citizen and your app in JSON format. You can have multiple citizen configuration files within this directory, allowing different configurations based on environment. citizen retrieves its configuration file from this directory based on the following logic:

  1. citizen parses each JSON file looking for a host key that matches the machine's hostname. If it finds one, it loads that configuration.
  2. If it can't find a matching host key, it looks for a file named citizen.json and loads that configuration.
  3. If it can't find citizen.json or you don't have a config directory, it runs under its default configuration.

Let's say you want to run citizen on port 8080 in your local dev environment and you have a local database your app will connect to. You could create a config file called local.json (or myconfig.json, whatever you want) with the following:

{
  "host":       "My-MacBook-Pro.local",
  "citizen": {
    "mode":     "development",
    "http": {
      "port":   8080
    }
  },
  "db": {
    "server":   "localhost",
    "username": "dbuser",
    "password": "dbpassword"
  }
}

This config would extend the default configuration only when running on your local machine. If you were to push this to a production environment accidentally, it wouldn't do anything. Using this method, you can commit multiple config files from different environments to the same repository.

The database settings would be accessible within your app via app.config.db. The citizen and host nodes are reserved for the framework; create your own node(s) to store your custom settings.

Inline config

You can also pass your app's configuration directly to citizen through app.start(). If there is a config file, an inline config will extend the config file. If there's no config file, the inline configuration extends the default citizen config.

// Start an HTTP server on port 8080 accepting requests at www.mysite.com
app.start({
  citizen: {
    http: {
      hostname: 'www.mysite.com',
      port:     8080
    }
  }
})

// Start an HTTPS server with key and cert PEM files
app.start({
  citizen: {
    http: {
      enable: false
    },
    https: {
      enable: true,
      key:    '/absolute/path/to/key.pem',
      cert:   '/absolute/path/to/cert.pem'
    }
  }
})

// Start an HTTPS server with a PFX file running on port 3000,
// and add a custom namespace for your app's database config
app.start({
  citizen: {
    http: {
      enable: false
    },
    https: {
      enable: true,
      port:   3000,
      pfx:    '/absolute/path/to/site.pfx'
    }
  },
  db: {
    server:   "localhost",  // app.config.db.server
    username: "dbuser",     // app.config.db.username
    password: "dbpassword"  // app.config.db.password
  }
})

Default configuration

The following represents citizen's default configuration, which is extended by your configuration:

{
  host                  : "",
  citizen: {
    mode                : "production",
    http: {
      enable            : true,
      hostname          : "127.0.0.1",
      port              : 80
    },
    https: {
      enable            : false,
      hostname          : "127.0.0.1",
      port              : 443,
      secureCookies     : true
    },
    connectionQueue     : null,
    fallbackController  : "",
    templateEngine      : "handlebars",
    compression: {
      enable            : false,
      force             : false,
      mimeTypes         : "text/plain text/html text/css application/x-javascript application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml"
    },
    sessions            : false,
    sessionTimeout      : 20,
    layout: {
      controller        : "",
      view              : ""
    },
    legalFormat: {
      html              : true,
      json              : false,
      jsonp             : false
    },
    form: {},
    cache: {
      application: {
        enable          : true,
        lifespan        : 15,
        resetOnAccess   : true,
        overwrite       : false,
        encoding        : "utf-8",
        synchronous     : false
      },
      static: {
        enable          : false,
        lifespan        : 15,
        resetOnAccess   : true
      },
      control           : {}
    },
    log: {
      console: {
        colors          : true,
        error           : false,
        status          : false
      },
      file: {
        error           : false,
        status          : false,
        maxFileSize     : 10000,
        watcher: {
          interval      : 60000
        }
      }
    },
    development: {
      debug: {
        scope: {
          config        : true,
          context       : true,
          cookie        : true,
          form          : true,
          payload       : true,
          request       : true,
          response      : true,
          route         : true,
          session       : true,
          url           : true,
        },
        depth           : 3,
        showHidden      : false,
        view            : false
      },
      enableCache       : false,
      watcher: {
        custom          : [],
        interval        : 500,
        killSession     : false
      }
    },
    urlPaths:  {
      app               : "/"
    },
    directories:  {
      app               : "[resolved based on location of start.js]",
      hooks             : "[directories.app]/hooks",
      logs              : "[directories.app]/logs",
      controllers       : "[directories.app]/patterns/controllers",
      models            : "[directories.app]/patterns/models",
      views             : "[directories.app]/patterns/views",
      web               : "web"
    }
  }
}

HTTPS

When starting an HTTPS server, in addition to the hostname and port options, citizen takes the same options as Node's https.createServer() (which takes the same options as tls.createServer()).

The only difference is how you pass key files. As you can see in the examples above, you pass citizen the file paths for your key files. citizen reads the files for you.

Config settings

Here's a complete rundown of citizen's settings and what they mean:

citizen config options
Setting Values Description
host

The operating system's hostname

To load different config files in different environments, citizen relies upon the server's hostname as a key. At startup, if citizen finds a config file with a host key that matches the server's hostname, it chooses that config file. This is different from the HTTP hostname setting under the citizen node (see below).
citizen
mode
  • production
  • development

Default: production

The application mode determines certain runtime behaviors. Production mode silences console logs and enables all application features. Development mode enables verbose console logs, disables caching, and enables the file watcher for hot module reloading for controllers, models, and hooks.
http
enable

Boolean

Default: true

This setting controls the HTTP server, which is enabled by default.
hostname

String

Default: 127.0.0.1

The hostname at which your app can be accessed via HTTP. The default is localhost, but you can specify an empty string to accept requests at any hostname.
port

Number

Default: 80

The port number on which citizen's HTTP server should listen for requests.
https
enable

Boolean

Default: false

This setting controls the HTTPS server, which is disabled by default.
hostname

String

Default: 127.0.0.1

The hostname at which your app can be accessed via HTTPS. The default is localhost, but you can specify an empty string to accept requests at any hostname.
port

Number

Default: 443

The port number on which citizen's HTTPS server should listen for requests.
secureCookies

Boolean

Default: true

By default, all cookies set during an HTTPS request are secure. Set this option to false to override that behavior, making all cookies insecure and requiring you to manually set the secure option in the cookie directive.
connectionQueue

A positive integer

Default: null

The maximum number of incoming requests to queue. If left unspecified, the operating system determines the queue limit.
sessions

Boolean

Default: false

Enables the user session scope, which assigns each visitor a unique ID and allows you to store data associated with that ID on the server.
sessionTimeout

Positive integer

Default: 20

If sessions are enabled, this number represents the length of a user's session in minutes. Sessions automatically expire if a user has been inactive for this amount of time.
layout
controller

String

Default: (empty)

If you use a global layout controller, you can specify the name of that controller here instead of using the handoff directive in all your controllers.
view

String

Default: (empty)

By default, the layout controller will use the default layout view, but you can specify a different view here. Use the file name without the file extension.
templateEngine

String

Default: handlebars

citizen installs Handlebars by default, but you can change the template engine to any engine supported by consolidate.js.
legalFormat
html

Boolean

Default: true

citizen provides HTML output by default based on your views. You can disable HTML entirely if you plan to use citizen for building an API that returns JSON or JSONP only.
json

Boolean

Default: false

JSON output is disabled by default. Set this value to true to enable global JSON output from all controllers. To enable JSON at the controller action level, see the legalFormat directive and JSON and JSONP for details.
jsonp

Boolean

Default: false

JSONP output is disabled by default. Set this value to true to enable global JSONP output from all controllers. To enable JSONP at the controller action level, see the legalFormat directive and JSON and JSONP for details.
form
{ options }

Object

Default: Same as formidable

citizen uses formidable to parse form data. The defaults match that of formidable. Provide settings in the form node to set default settings for all your forms. See Forms for details.
compression
enable

Boolean

Default: false

Enables gzip and deflate compression for rendered views and static assets.
force

Boolean or String

Default: false

Forces gzip or deflate encoding for all clients, even if they don't report accepting compressed formats. Many proxies and firewalls break the Accept-Encoding header that determines gzip support, and since all modern clients support gzip, it's usually safe to force it by setting this to gzip, but you can also force deflate.
mimeTypes

String

A space-delimited list of MIME types that should be compressed if compression is enabled. See the sample config above for the default list. If you want to add or remove items, you must replace the list in its entirety.
cache
application
enable

Boolean

Default: true

Enables the in-memory cache, accessed via the cache.set() and cache.get() methods. Set this to false to disable the cache when in production mode, which is useful for debugging when not in development mode.
lifespan

Number

Default: 15

The length of time a cached application asset remains in memory, in minutes.
resetOnAccess

Boolean

Default: true

Determines whether to reset the cache timer on a cached asset whenever the cache is accessed. When set to false, cached items expire when the lifespan is reached.
overwrite

Boolean

Default: false

Determines whether a call to cache.set() will overwrite an existing cache key. By default, an error is thrown if the cache key already exists. You can either pass the overwrite flag as an option in cache.set() or set this to true to always overwrite.
encoding

String

Default: utf-8

When you pass a file path to cache.set(), the encoding setting determines what encoding should be used when reading the file.
synchronous

Boolean

Default: false

When you pass a file path to cache.set(), this setting determines whether the file should be read synchronously or asynchronously. By default, file reads are asynchronous.
static
enable

Boolean

Default: false

When serving static files, citizen normally reads the file from disk for each request. You can speed up static file serving considerably by setting this to true, which caches file buffers in memory.
lifespan

Number

Default: 15

The length of time a cached static asset remains in memory, in minutes.
resetOnAccess

Boolean

Default: true

Determines whether to reset the cache timer on a cached static asset whenever the cache is accessed. When set to false, cached items expire when the lifespan is reached.
control

Key/value pairs

Default: {}

Use this setting to set Cache-Control headers for static assets. The key is the pathname of the asset, and the value is the Cache-Control header. See Client-Side Caching for details.
log
console
colors

Boolean

Default: true

Enables color coding in console logs.
error

Boolean

Default: false

Controls whether errors should be logged to the console.
status

Boolean

Default: false

Controls whether status messages should be logged to the console when in production mode. (Development mode always logs to the console.)
file
error

Boolean

Default: false

Controls whether errors should be logged to a file.
status

Boolean

Default: false

Controls whether status messages should be logged to a file. Applies to both development and production modes.
maxFileSize

Number

Default: 10000

Determines the maximum file size of log files in kilobytes. Default is 10000 (10 megs). When the limit is reached, the log file is renamed and a new log file is created.
watcher
interval

Number

Default: 60000

For operating systems that don't support file events, this timer (in milliseconds) determines how often log files will be polled for changes prior to archiving.
development
debug
scope

Object

This setting determines which scopes are logged in the debug output in development mode. By default, all scopes are enabled.
depth

Positive integer

Default: 3

When citizen dumps an object in the debug content, it inspects it using Node's util.inspect. This setting determines the depth of the inspection, meaning the number of nodes that will be inspected and displayed. Larger numbers mean deeper inspection and slower performance.
view

Boolean

Default: false

Set this to true to dump debug info directly into the HTML view.
enableCache

Boolean

Default: false

Development mode disables the cache. Change this setting to true to enable the cache in development mode.
watcher
custom

Array

You can tell citizen's hot module reloader to watch your own custom modules. This array can contain objects with watch (relative directory path to your modules within the app directory) and assign (the variable to which you assign these modules) properties. Example:

[ { "watch": "/toolbox", "assign": "app.toolbox" } ]
interval

Number

Default: 500

Determines the polling interval in milliseconds for hot module reloading on operating systems that don't support file system events. Shorter intervals are more CPU intensive.
urlPaths
app

String

Default: /

Denotes the URL path leading to your app. If you want your app to be accessible via http://yoursite.com/my/app and you're not using another server as a front end to proxy the request, this setting should be /my/app (don't forget the leading slash). This setting is required for the router to work.

These settings are exposed publicly via app.config.host and app.config.citizen.

Note: This documentation assumes your global app variable name is "app", but you can call it whatever you want. Adjust accordingly.

citizen methods and objects

app.start() Starts a citizen web application server.
app.cache.set()
app.cache.exists()
app.cache.get()
app.cache.clear()
app.log()
Helpers used internally by citizen, exposed publicly since you might find them useful.
app.controllers
app.models
Contains your supplied patterns, which you can use instead of require.
app.config The configuration settings you supplied at startup

Routing and URLs

The citizen URL structure determines which controller and action to fire, passes URL parameters, and makes a bit of room for SEO-friendly content that can double as a unique identifier. The structure looks like this:

http://www.site.com/controller/seo-content/action/myAction/param/val/param2/val2

For example, let's say your site's base URL is:

http://www.cleverna.me

The default controller is index, so the above is the equivalent of the following:

http://www.cleverna.me/index

If you have an article controller, you'd request it like this:

http://www.cleverna.me/article

Instead of query strings, citizen uses an SEO-friendly method of passing URL parameters consisting of name/value pairs. If you had to pass an article ID of 237 and a page number of 2, you'd append name/value pairs to the URL:

http://www.cleverna.me/article/id/237/page/2

Valid parameter names may contain letters, numbers, underscores, and dashes, but must start with a letter or underscore.

The default controller action is handler(), but you can specify alternate actions with the action parameter (more on this later):

http://www.cleverna.me/article/action/edit

citizen also lets you optionally insert relevant content into your URLs, like so:

http://www.cleverna.me/article/My-Clever-Article-Title/page/2

This SEO content must always follow the controller name and precede any name/value pairs. You can access it generically via route.descriptor or within the url scope (url.article in this case), which means you can use it as a unique identifier (more on URL parameters in the Controllers section).

The SEO content can consist of letters, numbers, dots (.), and the tilde (~) character.

MVC Patterns

citizen relies on a simple model-view-controller convention. The article pattern mentioned above might use the following structure:

app/
  patterns/
    controllers/
      article.js
    models/
      article.js
    views/
      article/       // Directory name matches the controller name
        article.hbs  // Default view name matches the controller name
        edit.hbs     // Secondary view, which must be called explicitly

At least one controller is required for a given URL, and a controller's default view directory and default view file must share its name. Additional views should reside in this same directory. More on views in the Views section.

Models and views are optional and don't necessarily need to be associated with a particular controller. If your controller doesn't need a model, you don't need to create one. If your controller is going to pass its output to another controller for further processing and final rendering, you don't need to include a matching view. (See the controller handoff directive.)

Controllers

A citizen controller is just a Node module. Each controller requires at least one public method to serve as an action for the requested route. The default action should be named handler(), which is called by citizen when no action is specified in the URL.

// article controller

module.exports = {
  handler: handler
}

// Default action
async function handler(params, context) {

  // Do some stuff

  return {
    // Send content and directives to the server
  }
}

The citizen server calls handler() after it processes the initial request and passes it 2 arguments: an object containing the parameters of the request and the current request's context.

Contents of the params argument
request The request object generated by the server, just in case you need direct access
response The response object generated by the server
route Details of the route, such as the requested URL and the name of the route (controller)
url Any URL parameters that were passed including the descriptor, if provided
form Data collected from a POST
payload Data collected from a PUT
cookie An object containing any cookies sent with the request
session An object containing any session variables, if sessions are enabled

In addition to having access to these objects within your controller, they are also included in your view context automatically so you can reference them within your view templates as local variables (more details in the Views section).

For example, based on the previous article URL...

http://www.cleverna.me/article/My-Clever-Article-Title/page/2

...you'll have the following params.url object passed to your controller:

{
  article: 'My-Clever-Article-Title',
  page: '2'
}

The controller name becomes a property in the URL scope that references the descriptor, which makes it well-suited for use as a unique identifier. This content is also available in the params.route object as route.descriptor.

The context argument contains any data that's been generated by the request up to this point. There are various events that can populate this argument with content and directives, which are then passed to your controller so you can access that content or see what directives have been set by previous events.

To return the results of the controller action, include a return statement with any content and directives you want to pass to citizen.

Using the above URL parameters, I can retrieve the article content from the model and pass it back to the server:

// article controller

module.exports = {
  handler: handler
}

async function handler(params, context) {
  // Get the article
  const article = await app.models.article.getArticle({
    article: params.url.article,
    page: params.url.page
  })
  const author = await app.models.article.getAuthor({
    author: article.author
  })

  // Return the article for view rendering using the content directive
  return {
    content: {
      article: article,
      author: author
    }
  }
}

Alternate actions can be requested using the action URL parameter. For example, maybe we want a different action and view to edit an article:

// http://www.cleverna.me/article/My-Clever-Article-Title/page/2/action/edit

// article controller

module.exports = {
  handler: handler,
  edit: edit
}

async function handler(params, context) {
  // Get the article
  const article = await app.models.article.getArticle({
    article: params.url.article,
    page: params.url.page
  })
  const author = await app.models.article.getAuthor({
    author: article.author
  })

  // Return the article for view rendering using the content directive
  return {
    content: {
      article: article,
      author: author
    }
  }
}

async function edit(params, context) {
  // Get the article
  const article = await app.models.article.getArticle({
    article: params.url.article,
    page: params.url.page
  })

  // Use the /patterns/views/article/edit.hbs view for this action (more on
  // alternate views in later sections).
  return {
    content: {
      article: article
    },
    view: 'edit'
  }
}

You place any data you want to pass back to citizen within the return statement. All the content you want to render in your view should be passed to citizen within an object called content, as shown above. Additional objects can be passed to citizen to set directives that provide instructions to the server (explained later in the Controller Directives section). You can even add your own objects to the request context and pass them from controller to controller (more in the Controller Handoff section.)

Private controllers

To make a controller private—inaccessible via HTTP, but accessible within your app—add a plus sign (+) to the beginning of the file name:

app/
  patterns/
    controllers/
      +_header.js  // Partial, only accessible internally
      _head.js     // Partial, accessible via www.cleverna.me/_head
      article.js   // Accessible via www.cleverna.me/article

Cross domain requests

If you want to make a controller action available to third parties, see the CORS section.

Models

Models are optional and their structure is completely up to you. citizen doesn't talk to your models directly; it only stores them in app.models for your convenience.

Here's what a simple model for the article pattern might look like:

// article model

module.exports = {
  getArticle: getArticle
}

async function getArticle(article, page) {
  const client = await app.db.connect()
  const result = await client.query({
    text: 'select title, summary, text, author, published from articles where id = $1 and page = $2;',
    values: [ article, page ]
  })
  client.release()

  return result.rows
}

async function getAuthor(author) {
  const client = await app.db.connect()
  const result = await client.query({
    text: 'select name, email from users where id = $1;',
    values: [ author ]
  })
  client.release()

  return result.rows
}

Views

citizen installs Handlebars by default, but you can install any engine supported by consolidate.js and set the templateEngine config setting accordingly. Make sure you use the correct file extension with your views so citizen knows how to parse them. citizen only supports one template engine at a time; you can't mix and match templates.

In article.hbs, you can reference objects you placed within the content object passed into the controller's return statement. citizen also injects the params object into your view context automatically, so you have access to those objects as local variables (such as the url scope):

{{! article.hbs }}

<!doctype html>
<html>
  <body>
    <main>
      <h1>
        {{article.title}} — Page {{url.page}}
      </h1>
      <h2>{{author.name}}, {{article.published}}</h2>
      <p>
        {{article.summary}}
      </p>
      <section>
        {{article.text}}
      </section>
    </main>
  </body>
</html>

Rendering alternate views

By default, the server renders the view whose name matches that of the controller. To render a different view, use the view directive in your return statement.

JSON and JSONP

You can tell a controller to return its content as JSON by adding the format URL parameter, letting the same resource serve both a complete HTML view and JSON for AJAX requests and RESTful APIs.

http://www.cleverna.me/article/My-Clever-Article-Title/page/2/format/json

Returns...

{
  "article": {
    "title": "My Clever Article Title",
    "summary": "Am I not terribly clever?",
    "text": "This is my article text."
  },
  "author": {
    "name": "John Smith",
    "email": "jsmith@cleverna.me"
  }
}

Whatever you've added to the controller's return statement content object will be returned.

To enable JSON output at the controller level:

return {
  content: {
    article: article,
    author: author
  },
  legalFormat: {
    json: true
  }
}

You can also specify indivdidual nodes to return instead of returning the entire content object by using the output URL parameter:

http://www.cleverna.me/article/My-Clever-Article-Title/page/2/format/json/output/author

Returns...

{
  "name": "John Smith",
  "email": "jsmith@cleverna.me"
}

Use dash notation in the output parameter to go deeper into the node tree:

http://www.cleverna.me/article/My-Clever-Article-Title/page/2/format/json/output/author-email

Returns...

jsmith@cleverna.me

For JSONP, use format paired with callback in the URL:

http://www.cleverna.me/article/My-Clever-Article-Title/format/jsonp/callback/foo

Returns:

foo({
  "article": {
    "title": "My Clever Article Title",
    "summary": "Am I not terribly clever?",
    "text": "This is my article text."
  },
  "author": {
    "name": "John Smith",
    "email": "jsmith@cleverna.me"
  }
})

To enable JSONP output at the controller level:

return {
  legalFormat: {
    jsonp: true
  }
}

The output URL parameter works with JSONP as well.

http://www.cleverna.me/article/My-Clever-Article-Title/page/2/format/jsonp/callback/foo/output/author-email

Returns...

foo("jsmith@cleverna.me")

Do you always want a particular controller action to return JSON without the URL flag? Simple:

async function handler(params) {

  // All requests handled by this controller action will output JSON
  params.route.format = 'json'

  return {
    content: {
      article: article,
      author: author
    },
    legalFormat: {
      json: true
    }
  }
}

Are you building a RESTful API and want every request across all controllers to return JSON without using the URL flag? Also simple, via optional hooks such as request.js:

// File: /app/hooks/request.js
// All requests will be returned in JSON format because this function runs
// at the beginning of every request. Learn more in the "Application Event Hooks
// and the Context Argument" section.

function start(params) {
  // Set the output format to JSON
  params.route.format = 'json'

  return {
    // Set JSON as a legal output format
    legalFormat: {
      json: true
    }
  }
}
JSON security risks

The JSON output from any controller action is driven by the returned content object. Anything you place in the content object will be present in the JSON output — whether it's present in the HTML view or not.

Take care not to place sensitive data in the content object thinking it will never be exposed because you don't reference it in your view template, because it WILL show up in your JSON.

Handling Errors

citizen does a great job of handling unexpected errors without exiting the process (and thereby crashing your server). The following controller action will throw an error, but the server will respond with a 500 and keep running:

async function handler(params, context) {
  // app.models.article.foo() throws an error
  const foo = await app.models.article.foo()

  return {
    content: foo
  }
}

You can also throw an error manually and customize the error message:

async function handler(params, context) {
  // Get the article
  const article = await app.models.article.getArticle({
    article: params.url.article,
    page: params.url.page
  })

  // If everything is fine, return the content
  if ( article ) {
    return {
      content: article
    }
  // If there's a problem, throw an error
  } else {
    let err = new Error('The requested article does not exist.')
    // The default error code is 500, but you can specify a different code
    err.statusCode = 404
    throw err
  }
}

Errors are returned in the format requested by the route. If you request JSON and the route throws an error, the error will be in JSON format.

The app skeleton created by the scaffold utility includes optional error view templates for common client and server errors, but you can create templates for any HTTP error code.

Error Views

To create custom error views for server errors, create a directory called /app/patterns/views/error and populate it with templates named after the HTTP response code or Node error code.

app/
  patterns/
    views/
      error/
        404.hbs      // Handles 404 errors
        500.hbs      // Handles 500 errors
        ENOENT.hbs   // Handles bad file read operations
        error.hbs    // Handles any error without its own template

These error views are only used when citizen is in production mode. In development mode, citizen dumps the raw error/stack to the view.

Controller Directives

In addition to the view content, the controller action's return statement can also pass directives to render alternate views, set cookies and session variables, initiate redirects, call and render includes, cache views (or entire routes), and hand off the request to another controller for further processing.

Alternate Views

By default, the server renders the view whose name matches that of the controller. To render a different view, use the view directive in your return statement:

// article controller

module.exports = {
  handler: handler
}

async function handler(params, context) {
  const article = await app.models.article.getArticle({
    article: params.url.article,
    page: params.url.page
  })

  return {
    content: article,
    // This tells the server to render app/patterns/views/article/edit.hbs
    view: 'edit'
  }
}

Formats

citizen returns HTML by default, and you can enable JSON or JSONP in the global config. However, if you want to enable custom formats at the controller action level, use the legalFormat directive.

If you don't enable a format, either in the controller action or global config, trying to use that format will throw an error.

return {
  legalFormat: {
    html: false,
    json: true,
    jsonp: true
  }
}

Cookies

You set cookies by returning a cookie object within the controller action.

Here's an example of a complete cookie object's default settings:

cookie.foo = {
  value: 'myValue',

  // Valid expiration options are:
  // 'now' - deletes an existing cookie
  // 'never' - current time plus 30 years, so effectively never
  // 'session' - expires at the end of the browser session (default)
  // [time in minutes] - expires this many minutes from now
  expires: 'session',

  // By default, a cookie's path is the same as the app path in your config
  path: app.config.citizen.urlPaths.app,

  // citizen's cookies are accessible via HTTP only by default. To access a
  // cookie via JavaScript, set this to false.
  httpOnly: true,

  // Cookies are insecure when set over HTTP and secure when set over HTTPS.
  // You can override that behavior globally with the https.secureCookies setting
  // in your config or on a case-by-case basis with this setting.
  secure: false
}

The following sample login controller tells the server to set username and passwordHash cookies that expire in 20 minutes:

// login controller

module.exports = {
  handler: handler
}

async function loginForm(params, context) {
  let authenticate = await app.models.login.authenticate({
        // Form values, just like URL parameters, are passed via the params
        // argument
        username: params.form.username,
        password: params.form.password
      }),
      // If a directive is an empty object, that's fine. citizen just ignores it.
      cookie = {}

  if ( authenticate.success ) {
    cookie = {
      // The cookie gets its name from the property name
      username: {
        value: authenticate.username,
        expires: 20
      },
      passwordHash: {
        value: authenticate.passwordHash,
        expires: 20
      }
    }
  }

  return {
    content: {
      authenticate: authenticate
    },
    cookie: cookie
  }
}

Alternatively, you can pass strings into the cookie directive, which will create cookies using the default attributes. The following code sets the same cookies, but they expire at the end of the browser session:

return {
  cookie: {
    username: authenticate.username,
    passwordHash: authenticate.passwordHash
  }
}

Cookies sent by the client are available in params.cookie within the controller and simply cookie within the view context:

<!doctype html>
<html>
  <body>
    <section>
      {{#if cookie.username}}
        Welcome, {{cookie.username}}.
      {{else}}
        <a href="/login">Login</a>
      {{/if}}
    </section>
  </body>
</html>

Cookie variables you set within your controller aren't immediately available within the params.cookie scope. citizen has to receive the context from the controller before it can send the cookie to the client, so use a local instance of the variable if you need to access it during the same request.

Proxy Header

If you use citizen behind another web server, such as NGINX or Apache, you'll need to add a custom header called x-citizen-uri for secure cookies to work correctly. This is because citizen only has access to the URI it receives in the proxy request, not the actual URI requested by the browser, which in the case of HTTPS requests will have a different protocol.

Here's an example of how you might set this up in NGINX:

location / {
  proxy_set_header X-Real-IP       $remote_addr
  proxy_set_header Host            $host
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for
  proxy_set_header x-citizen-uri   https://website.com$request_uri
  proxy_pass                       http://127.0.0.1:8080
}

Session Variables

If sessions are enabled, you can access session variables via params.session in your controller or simply session within views. These local scopes reference the current user's session without having to pass a session ID.

By default, a session has four properties: id, started, expires, and timer. The session ID is also sent to the client as a cookie called ctzn_sessionID.

Setting session variables is pretty much the same as setting cookie variables:

return {
  session: {
    username: 'Danny',
    nickname: 'Doc'
  }
}

Like cookies, session variables you've just assigned aren't available during the same request within the params.session scope, so use a local instance if you need to access this data right away.

Sessions expire based on the sessionTimeout config property, which represents the length of a session in minutes. The default is 20 minutes. The timer is reset with each request from the user. When the timer runs out, the session is deleted. Any client requests after that time will generate a new session ID and send a new session ID cookie to the client.

To forcibly clear and expire the current user's session:

return {
  session: {
    expires: 'now'
  }
}

Redirects

You can pass redirect instructions to the server that will be initiated after the controller action is processed.

The redirect object takes a URL string in its shorthand version, or three options: statusCode, url, and refresh. If you don't provide a status code, citizen uses 302 (temporary redirect). The refresh option determines whether the redirect uses a Location header or the non-standard Refresh header.

// Initiate a temporary redirect using the Location header
return {
  redirect: 'http://cleverna.me/login'
}

// Initiate a permanent redirect using the Refresh header, delaying the redirect
// by 5 seconds
return {
  redirect: {
    url: 'http://cleverna.me/new-url',
    statusCode: 301,
    refresh: 5
  }
}

Unlike the Location header, if you use the refresh option, citizen will send a rendered view to the client because the redirect occurs client-side.

Using the Location header breaks (in my opinion) the Referer header because the Referer ends up being not the resource that initiated the redirect, but the resource prior to the page that initiated it. To get around this problem, citizen stores a session variable called ctzn_referer that contains the URL of the resource that initiated the redirect, which you can use to redirect users properly. For example, if an unauthenticated user attempts to access a secure page and you redirect them to a login form, the address of the secure page will be stored in ctzn_referer so you can send them there instead of the page containing the link to the secure page.

If you haven't enabled sessions, citizen falls back to creating a cookie named ctzn_referer instead.

Proxy Header

If you use citizen behind another web server, such as NGINX or Apache, you'll need to add a custom header called x-citizen-uri for citizen_referer to work correctly. This is because citizen only has access to the URI it receives in the proxy request, not the actual URI requested by the browser. For example, using NGINX as an HTTPS front end means you'll have a different protocol in the requested URI than you'll have in the proxy URI. The x-citizen-uri header ensures citizen receives the original requested URL intact.

Here's an example of how you might set this up in NGINX:

location / {
  proxy_set_header X-Real-IP       $remote_addr
  proxy_set_header Host            $host
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for
  proxy_set_header x-citizen-uri   https://website.com$request_uri
  proxy_pass                       http://127.0.0.1:8080
}

HTTP Headers

You can set HTTP headers using the header directive:

return {
  header: {
    'Cache-Control':  'max-age=86400',
    'Date':           new Date().toISOString()
  }
}

Including Controllers

citizen lets you use complete MVC patterns as includes. These includes are more than just chunks of code that you can reuse because each has its own controller, model, and view(s). Here's the syntax:

async function handler(params, context) {
  return {
    include: {

      // The include name referenced in your view gets its name from the
      // property name. This include calls the _header controller with the
      // default action and view.
      header: {
        controller: '_header'
      },

      // This include calls the _footer controller using the "myAction" action.
      footer: {
        controller: '_footer',
        action: 'myAction'
      },

      // This include calls the _login-form controller using the "myAction"
      // action and "myView" view.
      loginForm: {
        controller: '_login-form',
        action: 'myAction',
        view: 'myView'
      },

      // This include calls the index controller, but processes it as if it
      // had been requested from a different URL. In this case, the include
      // will return JSON.
      index: {
        route: '/index/format/json'
      },

      // This include calls the index controller, but processes it as if it
      // had been requested from a different URL and uses an alternate view.
      index: {
        route: '/index/format/json',
        view: 'myView'
      }
    }
  }
}

Let's say our article pattern's template has the following contents. The head section contains dynamic meta data, and the header's content changes depending on whether the user is logged in or not:

<!doctype html>
<html>
  <head>
    <title>{{metaData.title}}</title>
    <meta name="description" content="{{metaData.description}}">
    <meta name="keywords" content="{{metaData.keywords}}">
    <link rel="stylesheet" type="text/css" href="site.css">
  </head>
  <body>
    <header>
      {{#if cookie.username}}
        <p>Welcome, {{cookie.username}}</p>
      {{else}}
        <a href="/login">Login</a>
      {{/if}}
    </header>
    <main>
      <h1>{{title}} — Page {{url.page}}</h1>
      <p>{{summary}}</p>
      <section>{{text}}</section>
    </main>
  </body>
</html>

It probably makes sense to use includes for the head section and header because you'll use that code everywhere, but rather than simple partials, you can create citizen includes. The head section can use its own model for populating the meta data, and since the header is different for authenticated users, let's pull that logic out of the view and put it in the header's controller. I like to follow the convention of starting partials with an underscore, but that's up to you:

app/
  patterns/
    controllers/
      _head.js
      _header.js  // Doesn't pull data, so it doesn't need a model
      article.js
    models/
      _head.js
      article.js
    views/
      _head/
        _head.hbs
      _header/
        _header.hbs
        _header-authenticated.hbs  // A different header for logged in users
      article/
        article.hbs

When the article controller is fired, it has to tell citizen which includes it needs. We do that with the include directive:

// article controller

module.exports = {
  handler: handler
}

async function handler(params, context) {
  // Get the article
  const article = await app.models.article.getArticle({
    article: params.url.article,
    page: params.url.page
  })

  return {
    content: article,
    include: {
      head: {
        // If only the controller is specified, the default action handler() is
        // called and the default view is rendered (_head.hbs in this case).
        controller: '_head'
      },
      header: {
        controller: '_header',
        // If the username cookie exists, use the authenticated action. If not,
        // use handler.
        action: params.cookie.username ? 'authenticated' : 'handler'

        // You can also specify the view here like this:
        // view: '_header-authenticated'
        // But we'll do that in the header controller instead. If you do specify
        // a view here, it will override whatever view is set in the controller.
      }
    }
  }
}

citizen include patterns have the same requirements as regular patterns, including a controller with a public action. The include directive above tells citizen to call the _head and _header controllers, pass them the same arguments that were passed to the article controller (params and context), render their respective views, and add the resulting views to the view context.

Here's what our head section controller might look like:

// _head controller

module.exports = {
  handler: handler
}

async function handler(params, context) {
  let metaData = await app.models._head({ article: params.url.article })

  return {
    content: metaData
  }
}

And the head section view:

<head>
  <title>{{title}}</title>
  <meta name="description" content="{{description}}">
  <meta name="keywords" content="{{keywords}}">
  <link rel="stylesheet" type="text/css" href="site.css">
</head>

Here's what our header controller might look like:

// _header controller

module.exports = {
  handler: handler,
  authenticated: authenticated
}

function handler(params, context) {
  return {
    view: '_header'
  }
}

function authenticated(params, context) {
  return {
    view: '_header-authenticated'
  }
}

And the header views:

{{! _header view (/patterns/views/_header/_header.hbs) }}

<header>
  <a href="/login">Login</a>
</header>


{{! _header-authenticated view  (/patterns/views/_header/_header-authenticated.hbs) }}

<header>
  <p>Welcome, {{cookie.username}}</p>
</header>

The rendered includes are stored in the include scope. The include object contains rendered HTML views, so you need to skip escaping ({{{...}}} in Handlebars) within the article view:

{{! article.hbs }}

<!doctype html>
<html>
  {{{include.head}}}
  <body>
    {{{include.header}}}
    <main>
      <h1>{{title}} — Page {{url.page}}</h1>
      <p>{{summary}}</p>
      <section>{{text}}</section>
    </main>
  </body>
</html>

citizen includes are self-contained. While they receive the same request context (URL parameters, form inputs, etc.) as the calling controller, content generated by the calling controller isn't automatically passed to its includes, and content generated inside an include isn't passed back to the caller. citizen includes also can't make use of cookie, session, redirect, or handoff directives, nor can they use their own includes (nested includes). Any of these directives within an include will be ignored. They only use the content directive (to populate their own views), the view directive for setting the view used by the include, and the cache directive for controller caching.

A pattern meant to be used as an include can be accessed via HTTP just like any other controller. You could request the _head controller like so:

http://cleverna.me/_head

Perhaps you'd have it return meta data as JSON for the article pattern:

// http://cleverna.me/_head/article/My-Clever-Article-Title/format/json

{
  "title": "My Clever Article Title",
  "description": "My article's description.",
  "keywords": "clever, article, keywords"
}

Of course, if you don't write the controller in a manner to accept direct requests and return content, it'll return nothing (or