Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

citizen

jaysylvester47MIT1.0.2

Node.js MVC web application framework. Includes routing, serving, caching, session management, and other helpful tools.

api server, application server, cache, caching, citizen, framework, mvc, server side, router, routing, view rendering, web application server, web server

readme

citizen

citizen is an MVC-based web application framework designed for people interested in quickly building fast, scalable web sites instead of digging around Node's guts or cobbling together a wobbly Jenga tower made out of 50 different packages.

Use citizen as the foundation for a traditional server-side web application, a modular single-page application (SPA), or a RESTful API.

There were numerous breaking changes in the transition from 0.9.x to 1.0.x. Please consult the changelog for an itemized list and review this updated documentation thoroughly.

Benefits

  • Convention over configuration, but still flexible
  • Zero-configuration server-side routing with SEO-friendly URLs
  • Server-side session management
  • Key/value store: cache requests, controller actions, objects, and static files
  • Simple directives for managing cookies, sessions, redirects, caches, and more
  • Powerful code reuse options via includes (components) and chaining
  • HTML, JSON, JSONP, and plain text served from the same pattern
  • ES module and Node (CommonJS) module support
  • Hot module replacement in development mode
  • View rendering using template literals or any engine supported by consolidate
  • Few direct dependencies

Clearly, this is way more content than any NPM/Github README should contain. I'm working on a site for this documentation.

Is it production ready?

I use citizen on my personal site and originaltrilogy.com. OT.com handles a moderate amount of traffic (a few hundred thousand views each month) on a $30 cloud hosting plan running a single instance of citizen, where the app/process runs for months at a time without 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 the web server:

$ mkdir myapp && cd myapp
$ npm install citizen
$ node node_modules/citizen/util/scaffold skeleton
$ node app/start.js

If everything went well, you'll see confirmation in the console that the web server is running. Go to http://127.0.0.1:3000 in your browser and you'll see a bare index template.

citizen uses template literals in its default template engine. You can install consolidate, update the template config, 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
  controllers/
    hooks/            // Application event hooks (optional)
      application.js
      request.js
      response.js
      session.js
    routes/           // Public route controllers
      index.js
  helpers/            // Utility modules (optional)
  models/             // Models (optional)
    index.js
  views/
    error/            // Default error views
      404.html
      500.html
      ENOENT.html
      error.html
    index.html        // Default index view
  start.js
logs/                 // Log files
  access.log
  error.log
web/                  // public static assets

Initializing citizen and starting the web server

Import citizen and start your app:

// start.js
import citizen from 'citizen'

global.app = citizen
app.start()

Run from the terminal:

$ node start.js

Configuration

You can configure your citizen app with a config file, startup options, and/or custom controller configurations.

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

  1. If citizen finds a config directory, it parses each JSON file looking for a host key that matches the machine's hostname, and if it finds one, extends the default configuration with the file config.
  2. If citizen can't find a matching host key, it looks for a file named citizen.json and loads that configuration.
  3. citizen then extends the config with your optional startup config.
  4. Individual route controllers and and actions can have their own custom config that further extends the app config.

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 dev.json, whatever you want) with the following:

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

This config would extend the default configuration only when running on your local machine. Using this method, you can commit multiple config files from different environments to the same repository.

The database settings would be accessible anywhere 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.

Startup configuration

You can set your app's configuration at startup through app.start(). If there is a config file, the startup config will extend the config file. If there's no config file, the startup configuration extends the default citizen config.

// Start an HTTPS server with a PFX file
app.start({
  citizen: {
    http: {
      enabled: false
    },
    https: {
      enabled: true,
      pfx:    '/absolute/path/to/site.pfx'
    }
  }
})

Controller configuration

To set custom configurations at the route controller level, export a config object (more on route controllers and actions in the route controllers section).

export const config = {
  // The "controller" property sets a configuration for all actions in this controller
  controller: {
    contentTypes: [ 'application/json' ]
  }

  // The "submit" property is only for the submit() controller action
  submit: {
    form: {
      maxPayloadSize: 1000000
    }
  }
}

Default configuration

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

{
  host                 : '',
  citizen: {
    mode               : process.env.NODE_ENV || 'production',
    global             : 'app',
    http: {
      enabled          : true,
      hostname         : '127.0.0.1',
      port             : 80
    },
    https: {
      enabled          : false,
      hostname         : '127.0.0.1',
      port             : 443,
      secureCookies    : true
    },
    connectionQueue    : null,
    templateEngine     : 'templateLiterals',
    compression: {
      enabled          : false,
      force            : false,
      mimeTypes        : [
                          'application/javascript',
                          'application/x-javascript',
                          'application/xml',
                          'application/xml+rss',
                          'image/svg+xml',
                          'text/css',
                          'text/html',
                          'text/javascript',
                          'text/plain',
                          'text/xml'
                          ]
    },
    sessions: {
      enabled          : false,
      lifespan         : 20 // minutes
    },
    layout: {
      controller       : '',
      view             : ''
    },
    contentTypes       : [
                          'text/html',
                          'text/plain',
                          'application/json',
                          'application/javascript'
                          ],
    forms: {
      enabled          : true,
      maxPayloadSize   : 524288 // 0.5MB
    },
    cache: {
      application: {
        enabled        : true,
        lifespan       : 15, // minutes
        resetOnAccess  : true,
        encoding       : 'utf-8',
        synchronous    : false
      },
      static: {
        enabled        : false,
        lifespan       : 15, // minutes
        resetOnAccess  : true
      },
      invalidUrlParams : 'warn',
      control          : {}
    },
    errors             : 'capture',
    logs: {
      access           : false, // performance-intensive, opt-in only
      error: {
        client         : true, // 400 errors
        server         : true // 500 errors
      },
      debug            : false,
      maxFileSize      : 10000,
      watcher: {
        interval       : 60000
      }
    },
    development: {
      debug: {
        scope: {
          config       : true,
          context      : true,
          cookie       : true,
          form         : true,
          payload      : true,
          route        : true,
          session      : true,
          url          : true,
        },
        depth          : 4,
        showHidden     : false,
        view           : false
      },
      watcher: {
        custom         : [],
        killSession    : false,
        ignored        : /(^|[/\\])\../ // Ignore dotfiles
      }
    },
    urlPath            : '/',
    directories: {
      app              : <appDirectory>,
      controllers      : <appDirectory> + '/controllers',
      helpers          : <appDirectory> + '/helpers',
      models           : <appDirectory> + '/models',
      views            : <appDirectory> + '/views',
      logs             : new URL('../../../logs', import.meta.url).pathname
      web              : new URL('../../../web', import.meta.url).pathname
    }
  }
}

Config settings

Here's a complete rundown of citizen's settings and what they do.

When starting a server, in addition to citizen's http and https config options, you can provide the same options as Node's http.createServer() and https.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.

citizen config options
Setting Type Default Value Description
host String '' 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 not to be confused with the HTTP server hostname (see below).
citizen
mode String Checks NODE_ENV first, otherwise production The application mode determines certain runtime behaviors. Possible values are production and development Production mode silences console logs. Development mode enables verbose console logs, URL debug options, and hot module replacement.
global String app The convention for initializing citizen in the start file assigns the framework to a global variable. The default, which you'll see referenced throughout the documentation, is app. You can change this setting if you want to use another name.
contentTypes Array [ 'text/html', 'text/plain', 'application/json', 'application/javascript' ] An allowlist of response formats for each request, based on the client's Accept request header. When configuring available formats for individual route controllers or actions, the entire array of available formats must be provided.
errors String capture When your application throws an error, the default behavior is for citizen to try to recover from the error and keep the application running. Setting this option to exit tells citizen to log the error and exit the process instead.
templateEngine String templateLiterals citizen uses [template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) syntax for view rendering by default. Optionally, you can install consolidate and use any engine it supports (for example, install Handlebars and set templateEngine to handlebars).
urlPath String / 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.
http
enabled Boolean true Enables the HTTP server.
hostname String 127.0.0.1 The hostname at which your app can be accessed via HTTP. You can specify an empty string to accept requests at any hostname.
port Number 3000 The port number on which citizen's HTTP server listens for requests.
https
enabled Boolean false Enables the HTTPS server.
hostname String 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 443 The port number on which citizen's HTTPS server listens for requests.
secureCookies Boolean true By default, all cookies set within 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 Integer null The maximum number of incoming requests to queue. If left unspecified, the operating system determines the queue limit.
sessions
enabled Boolean false Enables the user session scope, which assigns each visitor a unique ID and allows you to store data associated with that ID within the application server.
lifespan Positive Integer 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 '' If you use a global layout controller, you can specify the name of that controller here instead of using the next directive in all your controllers.
view String '' 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.
forms
enabled Boolean true citizen provides basic payload processing for simple forms. If you prefer to use a separate form package, set this to false.
maxPayloadSize Positive Integer 524288 Maximum form payload size, in bytes. Set a max payload size to prevent your server from being overloaded by form input data.
compression
enabled Boolean false Enables gzip and deflate compression for rendered views and static assets.
force Boolean or String 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 Array

See default config above.

An array of MIME types that will 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 array in its entirety.
cache
control Object containing key/value pairs {} Use this setting to set Cache-Control headers for route controllers and static assets. The key is the pathname of the asset, and the value is the Cache-Control header. See Client-Side Caching for details.
invalidUrlParams String warn The route cache option can specify valid URL parameters to prevent bad URLs from being cached, and invalidUrlParams determines whether to log a warning when encountering bad URLs or throw a client-side error. See Caching Requests and Controller Actions for details.
cache.application
enabled Boolean true Enables the in-memory cache, accessed via the cache.set() and cache.get() methods.
lifespan Number 15 The length of time a cached application asset remains in memory, in minutes.
resetOnAccess Boolean 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.
encoding String 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 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.
cache.static
enabled Boolean 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 15 The length of time a cached static asset remains in memory, in minutes.
resetOnAccess Boolean 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.
logs
access Boolean false Enables HTTP access log files. Disabled by default because access logs can explode quickly and ideally it should be handled by a web server.
debug Boolean false Enables debug log files. Useful for debugging production issues, but extremely verbose (the same logs you would see in the console in development mode).
maxFileSize Number 10000 Determines the maximum file size of log files, in kilobytes. When the limit is reached, the log file is renamed with a time stamp and a new log file is created.
logs.error
client Boolean true Enables logging of 400-level client errors.
server Boolean false Enables logging of 500-level server/application errors.
status Boolean false Controls whether status messages should be logged to the console when in production mode. (Development mode always logs to the console.)
logs.watcher
interval Number 60000 For operating systems that don't support file events, this timer determines how often log files will be polled for changes prior to archiving, in milliseconds.
development
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 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 false Set this to true to dump debug info directly into the HTML view.
enableCache Boolean false Development mode disables the cache. Change this setting to true to enable the cache in development mode.
development.watcher
custom Array You can tell citizen's hot module replacement 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": "/util", "assign": "app.util" } ]

citizen uses chokidar as its file watcher, so watcher option for both logs and development mode also accepts any option allowed by chokidar.

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

This documentation assumes your global app variable name is app. Adjust accordingly.

citizen exports

app.start() Starts a citizen web application server.
app.config The configuration settings you supplied at startup. citizen's settings are within app.config.citizen.
app.controllers
app.models
app.views
It's unlikely you'll need to access controllers and views directly, but referencing app.models instead of importing your models manually benefits from citizen's built-in hot module replacement.
app.helpers All helper/utility modules placed in app/helpers/ are imported into the helpers object.
app.cache.set()
app.cache.get()
app.cache.exists()
app.cache.clear()
Application cache and key/value store used internally by citizen, also available for your app.
app.log() Basic console and file logging used by citizen, exported for your use.

Routing and URLs

The citizen URL structure determines which route 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 route controller is index, and the default action is handler(), so the above is the equivalent of the following:

http://www.cleverna.me/index/action/handler

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

http://www.cleverna.me/article

Instead of query strings, citizen passes 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, including the controller action. 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 Route Controllers section).

Reserved words

The URL parameters action and direct are reserved for the framework, so don't use them for your app.

MVC Patterns

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

app/
  controllers/
    routes/
      article.js
  models/
    article.js    // Optional, name it whatever you want
  views/
    article.html  // The default view file name should match the controller name

At least one route controller is required for a given URL, and a route controller's default view file must share its name. Models are optional.

All views for a given route controller can exist in the app/views/ directory, or they can be placed in a directory whose name matches that of the controller for cleaner organization:

app/
  controllers/
    routes/
      article.js
    models/
      article.js
    views/
      article/
        article.html  // The default view
        edit.html     // Alternate article views
        delete.html

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 route 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 next directive).

Route Controllers

A citizen route controller is just a JavaScript module. Each route controller requires at least one export 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.

// Default route controller action

export const handler = async (params, request, response, 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 4 arguments: a params object containing the parameters of the request, the Node.js request and response objects, and the current request's context.

Properties of the params object
config Your app's configuration, including any customizations for the current controller action
route Details of the requested route, such as the URL and the name of the route controller
url Any parameters derived from the URL
form Data collected from a POST
payload The raw request payload
cookie Cookies sent with the request
session 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/id/237/page/2

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

{
  article: 'My-Clever-Article-Title',
  id: '237',
  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. It's also available in the params.route object as params.route.descriptor.

The context argument contains any data or directives that have been generated by previous controllers in the chain using their return statement.

To return the results of the controller action, include a return statement with any data 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

export const handler = async (params) => {
  // Get the article
  const article = await app.models.article.get({
    article: params.url.article,
    page: params.url.page
  })
  const author = await app.models.article.getAuthor({
    author: article.author
  })

  // Any data you want available to the view should be placed in the local directive
  return {
    local: {
      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/id/237/page/2/action/edit

// article controller

export const handler = async (params) => {
  // Get the article
  const article = await app.models.article.get({
    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 local directive
  return {
    local: {
      article: article,
      author: author
    }
  }
}

export const edit = async (params) => {
  // Get the article
  const article = await app.models.article.get({
    article: params.url.article,
    page: params.url.page
  })

  // Use the /views/article/edit.html view for this action
  return {
    local: {
      article: article
    },
    view: 'edit'
  }
}

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

Models

Models are optional modules 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. You can also import them manually into your controllers if you prefer.

The following function, when placed in app/models/article.js, will be accessible in your app via app.models.article.get():

// app.models.article.get()
export const get = async (id) => {

  let article = // do some stuff to retrieve the article from the db using the provided ID, then...

  return article
}

Views

citizen uses template literals for view rendering by default. You can install consolidate.js and use any supported template engine. Just update the templateEngine config setting accordingly.

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

<!-- article.html -->

<!doctype html>
<html>
  <body>
    <main>
      <h1>
        ${local.article.title} — Page ${url.page}
      </h1>
      <h2>${local.author.name}, ${local.article.published}</h2>
      <p>
        ${local.article.summary}
      </p>
      <section>
        ${local.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.

All views go in /app/views. If a controller has multiple views, you can organize them within a directory named after that controller.

app/
  controllers/
    routes/
      article.js
      index.js
  views/
    article/
      article.html  // Default article controller view
      edit.html
    index.html      // Default index controller view

JSON and JSON-P

You can tell a route controller to return its local variables as JSON or JSON-P by setting the appropriate HTTP Accept header in your request, letting the same resource serve both a complete HTML view and JSON for AJAX requests and RESTful APIs.

The article route controller handler() action would return:

{
  "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 local object will be returned.

For JSONP, use callback in the URL:

http://www.cleverna.me/article/My-Clever-Article-Title/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"
  }
});

Forcing a Content Type

To force a specific content type for a given request, set response.contentType in the route controller to your desired output:

export const handler = async (params, request, response) => {
  // Every request will receive a JSON response regardless of the Accept header
  response.contentType = 'application/json'
}

You can force a global response type across all requests within an event hook.

Helpers

Helpers are optional utility modules and their structure is completely up to you. They're stored in app.helpers for your convenience. You can also import them manually into your controllers and models if you prefer.

The following function, when placed in app/helpers/validate.js, will be accessible in your app via app.helpers.validate.email():

// app.helpers.validate.email()
export const email = (address) => {
  const emailRegex = new RegExp(/[a-z0-9!##$%&''*+/=?^_`{|}~-]+(?:\.[a-z0-9!##$%&''*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i)

  return emailRegex.test(address)
}

Hot Module Replacement

citizen stores all modules in the app scope not just for easy retrieval, but to support hot module replacement (HMR). When you save changes to any module or view in development mode, citizen clears the existing module import and re-imports that module in real time.

You'll see a console log noting the affected file, and your app will continue to run. No need to restart.

Error Handling

citizen does its best to handle errors gracefully without exiting the process. The following controller action will throw an error, but the server will respond with a 500 and keep running:

export const handler = async (params) => {
  // app.models.article.foo() doesn't exist, so this action will throw an error
  const foo = await app.models.article.foo(params.url.article)

  return {
    local: foo
  }
}

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

export const handler = async (params) => {
  // Get the article
  const article = await app.models.article.get({
    article: params.url.article,
    page: params.url.page
  })

  // If the article exists, return it
  if ( article ) {
    return {
      local: {
        article: article
      }
    }
  // If the article doesn't exist, throw a 404
  } else {
    // Error messages default to the standard HTTP Status Code response, but you can customize them.
    let err = new Error('The requested article does not exist.')

    // The HTTP status code defaults to 500, but you can specify your own
    err.statusCode = 404

    throw err
  }
}

Note that params.route.controller is updated from the requested controller to error, so any references in your app to the requested controller should take this into account.

Errors are returned in the format requested by the route. If you request JSON and the route throws an error, citizen will return the error 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.

Capture vs. Exit

citizen's default error handling method is capture, which attempts graceful recovery. If you'd prefer to exit the process after an error, change config.citizen.errors to exit.

// config file: exit the process after an error
{
  "citizen": {
    "errors": "exit"
  }
}

After the application error handler fires, citizen will exit the process.

Error Views

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

app/
  views/
    error/
      500.html      // Displays any 500-level error
      404.html      // Displays 404 errors specifically
      ENOENT.html   // Displays bad file read operations
      error.html    // Displays any error without its own template

Controller Directives

In addition to view data, the route 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 route controller actions/views (or entire requests), 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

export const edit = async (params) => {
  const article = await app.models.article.get({
    article: params.url.article,
    page: params.url.page
  })

  return {
    local: article,
    // This tells the server to render app/views/article/edit.html
    view: 'edit'
  }
}

Cookies

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

export const handler = async (params) => {
  return {
    cookie: {
      // Cookie shorthand sets a cookie called username using the default cookie properties
      username: params.form.username,

      // Sets a cookie called last_active that expires in 20 minutes
      last_active: {
        value: new Date().toISOString(),
        expires: 20
      }
    }
  }
}

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

myCookie = {
  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',

  path: '/',

  // citizen's cookies are accessible via HTTP/HTTPS 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
}

Once cookies are set on the client, they're available in params.cookie within controllers and simply cookie within the view:

<!doctype html>
<html>
  <body>
    <section>
      Welcome, ${cookie.username}.
    </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 and send the response to the client first, so use a local instance of the variable if you need to access it during the same request.

Reserved Words

All cookies set by citizen start with the ctzn_ prefix to avoid collisions. Don't start your cookie names with ctzn_ and you should have no problems.

Proxy Header

If you use citizen behind a proxy, such as NGINX or Apache, make sure you have an HTTP Forwarded header in your server configuration so citizen's handling of secure cookies works correctly.

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

location / {
  proxy_set_header Forwarded         "for=$remote_addr;host=$host;proto=$scheme;";
  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_session_id.

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 sessions.lifespan 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'
  }
}

Reserved Words

All session variables set by citizen start with the ctzn_ prefix to avoid collisions. Don't start your session variable names with ctzn_ and you should have no problems.

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: '/login'
}

// Initiate a permanent redirect using the Refresh header, delaying the redirect by 5 seconds
return {
  redirect: {
    url: '/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 previous 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 a proxy, such as NGINX or Apache, make sure you have an HTTP Forwarded header in your server configuration so ctzn_referer works correctly.

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

location / {
  proxy_set_header Forwarded         "for=$remote_addr;host=$host;proto=$scheme;";
  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()
  }
}

You can also set headers directly using Node's response.setHeader() method, but using citizen's header directive preserves those headers in the request cache, so they'll be applied whenever that controller action is pulled from the cache.

Includes (Components)

citizen lets you use complete MVC patterns as includes, which are citizen's version of components. Each has its own route controller, model, and view(s). Includes can be used to perform an action or return a complete rendered view. Any route controller can be an include.

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>${local.metaData.title}</title>
    <meta name="description" content="${local.metaData.description}">
    <meta name="keywords" content="${local.metaData.keywords}">
    <link rel="stylesheet" type="text/css" href="site.css">
  </head>
  <body>
    <header>
      ${ cookie.username ? '<p>Welcome, ' + cookie.username + '</p>' : '<a href="/login">Login</a>' }
    </header>
    <main>
      <h1>${local.article.title} — Page ${url.page}</h1>
      <p>${local.article.summary}</p>
      <section>${local.article.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/
  controllers/
    routes/
      _head.js
      _header.js
      article.js
  models/
    _head.js
    article.js
  views/
    _head.html
    _header/
      _header.html
      _header-authenticated.html  // A different header for logged in users
    article.html

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

// article controller

export const handler = async (params) => {
  // Get the article
  const article = await app.models.article.get({
    article: params.url.article,
    page: params.url.page
  })

  return {
    local: {
      article: article
    },
    include: {
      // Include shorthand is a string containing the pathname to the desired route controller
      _head: '/_head/action/article',

      // Long-form include notation can explicitly define a route controller, action, and view
      _header: {
        controller: '_header',

        // If the username cookie exists, use the authenticated action. If not, use the default action.
        action: params.cookie.username ? 'authenticated' : 'handler'
      }
    }
  }
}

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, request, response, 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

export const article = async (params) => {
  let metaData = await app.models._head({ article: params.url.article })

  return {
    local: {
      metaData: metaData
    }
  }
}

And the head section view:

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

Here's what our header controller might look like:

// _header controller

// No need for a return statement, and no need to specify the view
// because handler() renders the default view.
//
// Every route controller needs at least one action, even if it's empty.
export const handler = () => {}

export const authenticated = () => {
  return {
    view: '_header-authenticated'
  }
}

And the header views:

<!-- /views/_header/_header.html -->

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

 

<!-- /views/_header/_header-authenticated.html -->

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

The rendered includes are stored in the include scope:

<!-- /views/article.html -->

<!doctype html>
<html>
  ${include._head}
  <body>
    ${include._header}
    <main>
      <h1>${local.title} — Page ${url.page}</h1>
      <p>${local.summary}</p>
      <section>${local.text}</section>
    </main>
  </body>
</html>

citizen includes are self-contained and delivered to the calling controller as a fully-rendered view. While they receive the same data (URL parameters, form inputs, request context, etc.) as the calling controller, data generated inside an include isn't passed back to the caller.

A pattern meant to be used as an include can be accessed via HTTP just like any other route controller. You could request the _header controller like so and receive a chunk of HTML or JSON as a response:

http://cleverna.me/_header

This is great for handling the first request server-side and then updating content with a client-side library.

Should I use a citizen include or a view partial?

citizen includes provide rich functionality, but they do have limitations and can be overkill in certain situations.

  • Do you only need to share a chunk of markup across different views? Use a standard view partial as defined by whatever template engine you're using. The syntax is easy and you don't have to create a full MVC pattern like you would with a citizen include.
  • Do you need to loop over a chunk of markup to render a data set? The server processes citizen includes and returns them as fully-rendered HTML (or JSON), not compiled templates. You can't loop over them and inject data like you can with view partials. However, you can build an include that returns a complete data set and view.
  • Do you need the ability to render different includes based on logic? citizen includes can have multiple actions and views because they're full MVC patterns. Using a citizen include, you can call different actions and views based on logic and keep that logic in the controller where it belongs. Using view partials would require registering multiple partials and putting the logic in the view template.
  • Do you want the include to be accessible from the web? Since a citizen include has a route controller, you can request it via HTTP like any other controller and get back HTML, JSON, or JSONP, which is great for AJAX requests and client-side rendering.

Controller Chaining

citizen allows you to chain multiple route controllers together in series from a single request using the next directive. The requested controller passes its data and rendered view to a subsequent controller, adding its own data and rendering its own view.

You can string as many route controllers together in a single request as you'd like. Each route controller will have its data and view output stored in the params.route.chain object.

// The index controller accepts the initial request and hands off execution to the article controller
export const handler = async (params) => {
  let user = await app.models.user.getUser({ userID: params.url.userID })

  return {
    local: {
      user: user
    },

    // Shorthand for next is a string containing the pathname to the route controller.
    // URL paramaters in this route will be parsed and handed to the next controller.
    next: '/article/My-Article/id/5'

    // Or, you can be explicit, but without parameters
    next: {
      // Pass this request to app/controllers/routes/article.js
      controller: 'article',

      // Specifying the action is optional. The next controller will use its default action, handler(), unless you specify a different action here.
      action: 'handler',

      // Specifying the view is optional. The next controller will use its default view unless you tell it to use a different one.
      view: 'article'
    }

    // You can also pass custom directives and data.
    doSomething: true
  }
}

Each controller in the chain has access to the previous controller's context and views. The last controller in the chain provides the final rendered view. A layout controller with all your site's global elements is a common use for this.

// The article controller does its thing, then hands off execution to the _layout controller
export const handler = async (params, request, response, context) => {
  let article = await getArticle({ id: params.url.id })

  // The context from the previous controller is available to you in the current controller.
  if ( context.doSomething ) {  // Or, params.route.chain.index.context
    await doSomething()
  }

  return {
    local: {
      article: article
    },
    next: '/_layout'
  }
}

The rendered view of each controller in the chain is stored in the route.chain object:

<!-- index.html, which is stored in route.chain.index.output -->
<h1>Welcome, ${local.user.username}!</h1>

<!-- article.html, which is stored in route.chain.article.output -->
<h1>${local.article.title}</h1>
<p>${local.article.summary}</p>
<section>${local.article.text}</section>

The layout controller handles the includes and renders its own view. Because it's the last controller in the chain, this rendered view is what will be sent to the client.

// _layout controller

export const handler = async (params) => {
  return {
    include: {
      _head: '/_head',
      _header: {
        controller: '_header',
        action: params.cookie.username ? 'authenticated' : 'handler'
      },
      _footer: '/_footer
    }
  }
}

 

<!-- _layout.html -->
<!doctype html>
<html>
  ${include._head}
  <body>
    ${include._header}
    <main>
      <!-- You can render each controller's view explicitly -->
      ${route.chain.index.output}
      ${route.chain.article.output}

      <!-- Or, you can loop over the route.chain object to output the view from each controller in the chain -->
      ${Object.keys(route.chain).map( controller => { return route.chain[controller].output }).join('')}
    </main>
    ${include._footer}
  </body>
</html>

You can skip rendering a controller's view in the chain by setting the view directive to false:

// This controller action won't render a view
export const handler = async () => {
  return {
    view: false,
    next: '/_layout'
  }
}

To bypass next in a request, add /direct/true to the URL.

http://cleverna.me/index/direct/true

The requested route controller's next directive will be ignored and its view will be returned to the client directly.

Default Layout

As mentioned in the config section at the beginning of this document, you can specify a default layout controller in your config so you don't have to insert it at the end of every controller chain:

{
  "citizen": {
    "layout": {
      "controller": "_layout",
      "view":       "_layout"
    }
  }
}

If you use this method, there's no need to use next for the layout. The last controller in the chain will always hand the request to the layout controller for final rendering.

Performance

citizen provides several ways for you to improve your app's performance, most of which come at the cost of system resources (memory or CPU).

Caching Requests and Controller Actions

In many cases, a requested URL or route controller action will generate the same view every time based on the same input parameters, so it doesn't make sense to run the controller chain and render the view from scratch for each request. citizen provides flexible caching capabilities to speed up your server side rendering via the cache directive.

cache.request

If a given request (URL) will result in the exact same rendered view with every request, you can cache that request with the request property. This is the fastest cache option because it pulls a fully rendered view from memory and skips all controller processing.

Let's say you chain the index, article, and layout controllers like we did above. If you put the following cache directive in your index controller, the requested URL's response will be cached and subsequent requests will skip the index, article, and layout controllers entirely.

return {
  next: '/article',
  cache: {
    request: true
  }
}

For the request cache directive to work, it must be placed in the first controller in the chain; in other words, the original requested route controller (index in this case). It will be ignored in any subsequent controllers.

The URL serves as the cache key, so each of the following URLs would generate

changelog

1.0.2

  • Rewrote config init to accommodate an empty config directory error
  • README updates and fixes

1.0.1

  • Fixed an unhandled promise rejection caused by trying to set session variables when no session is present
  • Minor README and development log tweaks

1.0.0

New features:

  • ES module support
  • Route controllers and actions have their own config that extends the global config

Enhancements/fixes:

  • citizen checks for the NODE_ENV environment variable and sets its mode accordingly
    • If NODE_ENV is undefined, citizen defaults to production mode
    • Setting the mode manually in your citizen configuration overrides NODE_ENV
  • Hot module replacement now works with the app cache enabled
    • Caching is now enabled by default to maintain consistency between development and production environments, but can still be disabled manually via the config for debugging purposes
  • Log files are split into access.log, error.log, and debug.log and can be enabled independently
    • If running only a single citizen app server instance, access logs will likely affect performance
    • Debug logs mimic development mode console logs, thus are extremely verbose and peformance-intensive
    • Client (400) and server (500) error logs can be enabled/disabled independently
  • citizen now uses the HTTP Accept header to determine the response format
    • Supported response types are text/html, text/plain, application/json, and application/javascript (JSON-P)
  • app.log() will attempt to create the logs directory if it doesn't already exist
  • Handling of 500 errors can be configured
    • capture (default) will write the error to the log, render an error view to the client, and keep the process/server running
    • exit will send a 500 to the client without rendering a view, write the error to the log, throw the error, then exit the process
  • The previously required x-citizen-uri header for routing behind proxy servers is deprecated in favor of industry standard Forwarded headers
    • X-Forwarded header support has been deprecated and will be removed from future versions
  • Previously reserved URL parameters (type, ajax, show, and task) have been released for dev use
  • JSON requests now provide the local context of the entire controller chain and all includes in the response
  • app.cache.set() now automatically expires/clears the specified cache key if it already exists and creates a new cache item rather than throwing an error
  • The file watcher, which relies upon chokidar, now accepts all settings available in chokidar via a new options configuration
    • Please reference the chokidar documentation for available options if your environment requires customizations (usePolling for networked file systems, for example)
  • The next directive (previously handoff) now accepts a route pathname similar to the include route pathname, which allows simulating a different route when handing off to the next controller in the chain
  • Views no longer require subfolders
    • Old pattern (still supported): views for a given route controller go within a folder matching that controller name, with the default view also matching the controller name
    • New pattern: views can reside within the main views folder directly, with the default view for a controller matching the controller name, so controllers with a single view no longer require a subfolder
  • Request and controller action caching are bypassed in the event of an application (500) error

Breaking changes

  • New default directory structure, but you can keep the old structure by editing the directory config
  • The default rendering engine is now based on template literals, and consolidate is no longer included as a dependency by default
    • To use another template engine, install consolidate and your preferred package (handlebars, pug, etc.), then update config.citizen.templateEngine with the package name
  • The handoff directive has been renamed to next
  • The route property has been removed from the include directive
    • Route controller includes now accept a pathname string as shorthand for an included route
  • Controller and request cache directives have changed in format and functionality
    • Cache directive properties are now action (formerly controller) and request (formerly route)
    • Cache request only applies if the controller action in which it's called is the action specified in the original request; subsequent controllers in the chain can no longer prompt a request cache
  • The /type/direct URL parameter used to bypass controller handoff has been replaced with /direct/true
  • The /direct/true URL parameter is no longer required to bypass the controller chain
    • Follow the partial naming convention by putting an underscore (_) at the beginning of the controller file name, and when requested from the client it will be rendered directly
    • /direct/true is still available to force controllers to bypass the chain
  • The server request.start() event now fires before checking if the controller exists
    • This is logically consistent with the intention behind request.start() (i.e., it fires at the start of the request)
    • This allows you to incorporate logic into request.start() even if the requested controller doesn't exist (custom 404 handling, for example)
    • This is considered a breaking change because the request.start() context won't inherit the controller config like it did previously, so if you depend on anything in your controller config within the request.start() event, that functionality should be moved to the controller action itself, which fires after request.start()
  • All instances of the enable property in the config have been renamed to enabled
  • The sessions configuration setting is now an object that takes two arguments:
    • enabled (boolean)
    • lifespan (integer) - Reflects session timeout in minutes and replaces the old sessionTimeout property, which has been removed
  • session.end() now requires an object containing key and value properties to end a session
  • The form configuration setting has been renamed to forms
    • The dependency on formidable has been removed and replaced with basic native form parsing, see the docs for settings/options
    • Third-party form processors can still be used within hooks and controllers by accessing Node's native request object, which is passed to each hook and controller as an argument
  • The log configuration setting has been renamed to logs
    • It now only applies to file logging, which can be enabled in development or production mode
    • Console debug logging is automatically enabled in development mode
  • The urlPaths configuration option has been removed
    • It never worked reliably, and this sort of thing should really be handled by a proxy anyway
  • The content directive, which contains all data local to the controller/view, has been renamed to local
    • Local variables within views should reference the local namespace (local.myVar)
  • The legalFormat config option is now contentTypes
    • contentTypes is an array that lists available output MIME types ("text/html", "text/plain", "application/json", "application/javascript")
  • The legalFormat directive has been removed
    • The new controller config mentioned above accepts the contentTypes option for setting available formats within a controller/action
  • The format URL parameter (/format/json, etc.) has been removed
    • To request different output formats, the client must set the HTTP Accept request header to the desired content type (currently supported: text/html, text/plain, application/json, application/javascript)
  • The request and response objects have been separated from the params object and are now passed into controllers as separate arguments
  • params.route no longer contains the view, but it was wrong half the time anyway
    • You can reference params.route.chain for all controllers in the chain, including their actions, views, and context
  • params.route.parsed.path is now params.route.parsed.pathname or params.route.pathname
  • Controller action CORS configuration has been incorporated into the new controller/action configuration feature
  • The output URL parameter for JSON requests has been removed
    • It added processing time and made view rendering more complex
    • The solutions to the problem solved by output include accessing controllers directly (underscore naming convention or /direct/true) and better API design in the first place
  • JSON requests are now namespaced using the route controller name
    • A request that would have returned { "foo": "bar" } previously will now return { "index" : { "foo": "bar" } }
  • The cache.set() overwrite option has been removed, as has the error/warning that occurred when attempting to overwrite existing cache items
  • The file watcher options have changed to match chokidar's options, so adjust accordingly
  • The ctzn_debug URL parameter is now a boolean that enables debug output in the view
    • Use the ctzn_inspect URL parameter to specify the variable you want to dump to the view
  • The undocumented fallbackController option has been removed because there are better ways to handle a nonexistent controller in the app itself (citizen now returns a 404 by default)

0.9.2

  • Moved commander to dependencies

0.9.1

  • Added SameSite cookie attribute to the cookie directive (defaults to "Lax" as recommended by the spec)
  • Fixed broken debug output
  • Removed public helper methods that were deprecated in 0.8.0 and supposed to have been removed in 0.9.0
  • Fixed typos in README
  • Better handling of connections closed by client

0.9.0

New features:

  • async-await support in controller actions
  • Hot module reloading for controllers and models in development mode

    BREAKING CHANGES (see readme for details)

  • Controller actions are now called as async functions, so you should use async-await syntax
  • Thanks to the above, the manual passing of event emitters to return results and errors is no longer supported and has been removed (simple return and throw statements do the job now). This is a major breaking change that will require you to rewrite all your controllers and any function calls to which you pass the emitter. While this could be a massive headache depending on the size of your app, it's unquestionably an improvement, with simpler syntax and no clunky emitter handling.
  • All helpers deprecated in 0.8.0 have been removed
  • The syntax for the formats directive has been simplified and it has been renamed to "legalFormat" to distinguish it from the format url parameter
  • The urlDelimiter option has been removed from JSON output (the delimiter is now a comma, because if you use a comma in a JSON key, you're insane)
  • The headers directive has been renamed to "header", keeping the grammar consistent with other directives (cookie, session, etc.)
  • Form configuration now has both a global config (set within your config file) and controller-action config (a new config object set within the controller module exports); syntax has changed from the previous global setting and maxFieldSize is now specified in kilobytes rather than megabytes
  • CORS controller settings are under the new config module export
  • Functionality previously under the debug mode has been moved to development mode and debug has been removed
  • Log configuration has been streamlined and consolidated, with some log settings previously under the debug settings now moved to logs
  • Invalid URL parameters in controller and route caches now always throw an error, but don't prevent rendering
  • The directory for app event handlers has been renamed from "/on" to "/hooks".

0.8.8

  • Fixed another bug in route cache retrieval

0.8.7

  • Fixed a bug in route cache retrieval
  • Tweaked readme

0.8.6

  • Added request context to console output in the event of an error to assist with debugging
  • Relaxed requirements on the route descriptor to allow dot (.) and tilde (~) characters
  • Added event handler for connections closed by the client

0.8.5

  • Added custom header check (x-citizen-uri) to provide the original requested URL to citizen when behind a proxy (nginx, apache, etc.), which fixes issues with ctzn_referer and secure cookies

0.8.4

  • Fixed a bug in static file serving that caused initial requests for compressed static files to fail

0.8.3

  • Removed unnecessary copy() on file cache retrieval, which was causing static file serving to bomb out

0.8.2

  • Preserve dates when copying objects

0.8.1

  • Further fixes to copy() and extend(). Circular references are no longer supported, however.
  • Removed a lot of unnecessary and complex copy() and extend() calls for a noticeable performance boost. There's less protection now against bad behavior (modifying param.session directly within controllers, for example), so if you access the params argument directly...test thoroughly :)

0.8.0

  • BREAKING CHANGES (see readme for details):
    • Updated dependencies to latest versions
    • The built-in Jade and Handlebars template engines have been replaced with consolidate.js (https://github.com/tj/consolidate.js). Simply install your preferred engine, add it to your dependencies, and set the "templateEngine" config setting. Handlebars is included by default.
    • Config change: the server "hostname" setting has been changed to "host" so as to avoid confusion with the HTTP/HTTPS server hostname settings
    • The copy() and extend() helpers have been updated for improved accuracy within the framework, which means some bad behavior that was tolerated previously will no longer work. Please test any code that relies on these methods to ensure it still behaves as expected.
    • CORS support has been enhanced to provide unique attributes to each controller action, so the notation in the controller has changed
    • Cache-Control settings for cached routes should now be handled using the new HTTP Header directive rather than route.cache.control
  • Deprecations
    • All helpers except for cache() and log() have been deprecated and will be removed from the public app scope in 0.9.0. Any of the deprecated helpers can be replaced by native functionality (Promises in place of listen() for example) or a third-party library. cache() and log() are still pretty nice though (imho), so I'm keeping them public.

0.7.19

  • Deprecations (these items will no longer be supported by citizen as of v0.8.0):
    • The "access" module export that enables CORS support at the controller level will be replaced with a "headers" directive that works at the controller action level.
    • The developers of Jade have renamed the project to Pug with all new updates being performed under the Pug name. citizen will drop support for Jade and replace it with the new Pug engine. There are a few breaking changes in Pug: https://github.com/pugjs/pug/issues/2305
  • Fixed an issue where "Accept-Encoding: gzip;q=0" would still provide gzip encoding because it was the only encoding specified (encoding now defaults to identity)
  • Official minimum node version has been bumped to 6.11.x

0.7.18

  • Opinion: See 0.7.17.

0.7.17

  • Opinion: requiring minor version bumps because of readme updates is stupid.

0.7.16

  • The accepting-encoding parser has been rewritten to be conformant
  • The compression engine has been rewritten to support deflate
  • A potentially breaking change: The default configuration has been changed to disable JSON and JSONP global output by default, which is safer. You can enable each one individually in the global config, causing all controllers to allow output in those formats, or enable JSON or JSONP output at the controller level using the new "formats" parameter.
  • HTML output can now be disabled completely at the global config level or controller level
  • Pretty output for HTML and JSON is now driven by the framework mode rather than config values. In debug and development modes, output is pretty. In production mode, output is ugly (minified).

0.7.15

  • Added referrer ("referer") to error log, because it's helpful

0.7.14

  • Fixed app crashes caused by non-request related errors (packages running in the background, for example)

0.7.13

  • Added the session scope to the beginning of the request chain, making it available within the application request event

0.7.12

  • Fixed a bug masked by the previous cache bug, which caused a timer to be set even if lifespan was set to "application"

0.7.11

  • Fixed a bug that caused a cache error when lifespan is set to "application" and tweaked the readme to reflect new cache defaults

0.7.10

  • Fixed a bug in the previous update that caused it not to work at all :)

0.7.9

  • The server requestStart event now fires before the sessionStart event, which is a more logical execution order. A 404 (missing controller and/or action) initiated by a third party referrer will no longer create an orphaned session, for example. This also allows custom handling of missing controllers and actions in the request.js start() action (like issuing a permanent redirect to a different controller or action, for example).

0.7.8

  • Cache-Control headers specified in the config (cache.control) now support regular expressions. You can provide either an exact match to the resource's pathname (the previous functionality) or a regex that matches many assets. You can mix the two. Also corrected the readme, which had an incorrect example for route Cache-Control.
  • Server errors now include the remote host

0.7.7

  • The server now throws an error if the view specified in a controller handoff doesn't exist. Previously, it failed silently and rendered the final view without the handoff view contents.

0.7.6

  • Fixed a bug that always enabled the static cache (introduced in 0.7.4 with the new cache config defaults)

0.7.5

  • Improved cache and session performance by getting rid of the creation of new timers with every request or cache hit and using a lastAccessed attribute for the existing timer to validate against (see https://github.com/jaysylvester/citizen/issues/31)

0.7.4

  • BREAKING CHANGE: Added default cache config options for both application and static asset caches, breaking previous config settings. The new defaults enable a reasonable cache lifespan (15 minutes), preventing issues like cached static assets from growing over time and taking up memory even if they're not being accessed regularly. Previously, enabling the static cache kept all static assets in memory for the life of the application. Consult the readme for details.
  • Improved server error handling, making apps more resilient to fatal errors and avoiding app crashes

0.7.3

  • BREAKING CHANGES (These were intended for 0.7.0, but were lost in the merge and I missed it. Sorry for the breakage in a point release.):
    • All config options can now be overwritten at app startup, including config settings in your own namespaces. This breaks the HTTP and HTTPS overwrite options that were available previously.
    • By default, app.start() starts an HTTP server. To start an HTTPS server, you must enable it manually inline or via a config file and provide key/cert/pfx files. You can disable HTTP entirely via config as well. Consult the readme for details.
    • HTTPS key/cert/pfx files should now be passed as a string representing the path to the file, not the contents of the files themselves. citizen reads these files itself now.

0.7.2

  • Removed errant console.log() from the server module

0.7.1

  • Fixed a bug in the cache module that caused an error when attempting to clear the file cache

0.7.0

  • Cache methods have been renamed and moved to their own namespace (app.cache). The old calls and their new equivalents are:
    • app.cache() -> app.cache.set()
    • app.retrieve() -> app.cache.get()
    • app.exists() -> app.cache.exists()
    • app.clear() -> app.cache.clear()
  • Fixe a bug in cache.get() that returned the parent of the specified scope rather than the scope itself when requesting an entire scope. This will break existing calls to cache.get({ scope: 'myScopeName' })
  • Cache lifespan should now be specified in minutes instead of milliseconds -> app.cache.set({ key: 'myKey', lifespan: 15 })
  • Config settings for sessionTimeout and requestTimeout should now be specified in minutes instead of milliseconds -> sessionTimeout: 20
  • Cookie expiration should now be specified in minutes instead of milliseconds
  • Added global cache setting to enable/disable cache in any mode
  • Added layout pattern setting to the config, which allows you to specify a default controller/view pairing as a layout controller and skip the handoff directive in the requested controller
  • Added per-controller form settings in the config, which breaks the previous form config method.
  • Added individual settings for application and static asset error and status logging. For example, you can now log application error messages in production mode without also logging static errors (404) or the typically verbose framework status messages.
  • listen() now accepts a single argument consisting of an object containing functions (flow and callback are optional)
  • Added ability to end a specific user session based on a session property key/value pair using app.session.end('key', 'value')
  • Specifying { view: false } in a controller's emitter during a handoff skips rendering of that controller's default view
  • All citizen-scoped cookie and session variables (starting with "ctzn") have been changed to "ctzn_camelCase" format (ctznReferer is now ctzn_referer).
  • dashes(), isInteger(), and isFloat() have been removed from helpers. There are libraries better suited for this.

0.6.9

  • Added listen() skip and end events to the readme.
  • Methods skipped in listen() by a previous method throwing an error now have their statuses correctly set to "skipped" in output.listen.status

0.6.8

  • The listen() error event has been returned to its previous behavior of throwing its own error. Without this, debugging is pretty much impossible. citizen's behavior remains the same, however (errors are not allowed to bubble up to the node process, so the app doesn't crash). You should still be using listen.success and listen.status to handle errors properly though.
  • BREAKING CHANGE: All URL parameters are now cast as strings, including numeric values. There are too many complicating factors in JavaScript's handling of numbers (floats, large integers) for citizen to make assumptions regarding the desired type. For example, "0123" is numeric and would have been stored as "123" under the previous convention; if your app was expecting the leading zero to remain intact, that would be bad. New convenience functions have been added (isInteger and isFloat) to assist in dealing with numbers.
  • The dashes() helper has been deprecated. It's still there, but will be gone in version 0.7.0. There are much more fully-featured URL slug packages available, and since citizen doesn't use this helper internally, there's no sense in keeping it.

0.6.7

  • Further stabilization of the error API.

0.6.6

  • Error handling has been further improved. See "Handling Errors" in the readme. Changes include:
    • Error response formats now match the requested route format (HTML, JSON, or JSONP), which is potentially a breaking change depending on how you've been handling errors up to this point.
    • The listen() error and timeout events no longer throw errors themselves, which is potentially a breaking change if you were relying on these thrown errors previously. You should now use the listen() status output (output.listen) for dealing with the results of these events.

0.6.5

  • Added an error event to the listen() emitter. Any methods that rely on emitters (including your controllers) can now emit errors with HTTP status codes and error messages. See "Controllers" and "listen()" in the readme.
  • listen() now reports status in its output (output.listen) for individual methods. Combine this with the emitter's error event to help track and recover from errors in asynchronous function calls. See "listen()" in the readme.
  • The redirect directive now accepts a URL string as shorthand for a temporary redirect using the Location header

0.6.4

  • BREAKING CHANGE: The JSON/JSONP output parameter can now return nodes more than one level deep. See "JSON" in the readme for details. The breakage occurs due to how the JSON is returned; if you specify only the first top-level node, just the value of that node is returned within an envelope. Previously, the envelope contained a single key (the top-level node name) containing the value of the node.
  • JSON/JSONP output is now pretty by default. Remove whitespace by setting "pretty" to false in the config.
  • Fixed PUT/PATCH/DELETE methods to process payloads consisting of JSON or form data
  • Controller includes can now be specified via a "route" option, making it possible to call includes with different parameters than the parent controller. See "Including Controllers" in the readme.

0.6.3

  • Fixed another bug in controller caching. Dur.

0.6.2

  • Fixed a bug in controller caching that threw a server error
  • Updated the readme to reflect breaking changes introduced in 0.6.0 that I somehow missed.

0.6.1

  • Added ETag and Cache-Control headers to cached routes and static files. See "Client-Side Caching" in the readme.
  • Minor breaking change due to a bug fix: the app's urlPath (config.urlPaths.app) is now removed from params.route.filePath for static resources to provide an accurate file path from your public web root. The appropriate replacement is params.route.pathname, which includes the entire path after your site's domain.

0.6.0

  • BREAKING CHANGE: The nesting syntax for the cache directive has changed slightly, but updating your code will be easy (mostly copy/paste of your existing cache directives). See "Caching Routes and Controllers" in the readme.
  • BREAKING CHANGE: File cache retrieval syntax has changed. You must now specify the file attribute rather than the key attribute: app.exists({ file: '/path/to/file.txt' }); app.retrieve({ file: '/path/to/file.txt' }); app.exists({ file: 'myCustomKey' }); app.retrieve({ file: 'myCustomKey' });
  • gzip support added for both dynamic and static resources. See configuration options in the readme.
  • Static asset caching added. See configuration options in the readme.

0.5.10

  • You can now specify a node name in the URL to return a specific top-level JSON node (/format/json/output/nodename). Works for JSONP also. See JSON in the readme for instructions.
  • Added Forms section to readme to provide some tips on working with form data and creating progressively enhanced forms
  • citizen's reserved URL parameters no longer need to be added to cache.directives manually
  • Added an option to log a warning about invalid cache URL parameters instead of throwing an error. If citizen encounters a URL parameter not present in cache.urlParams, the route will be processed and displayed, but not cached. This is now the default behavior. Use "cache": { "invalidUrlParams" : "throw" } in your config to throw an error instead.
  • The server now throws an error if you try to assign values to any reserved directive names that will cause problems in your application; scopes include url, session, content, and others. I'd label this a breaking change, but if you're doing this, your application is probably already broken.

0.5.9

  • app.listen() has been enhanced to perform asynchronous parallel, series, or waterfall processing. This is a non-breaking change because the default behavior (parallel) is the same as the original behavior. See listen() in the readme for details.
  • Replaced POST body parsing with formidable. I hate adding dependencies, but formidable is the standard and it works very well, so it seems silly to write a multipart form parser from scratch.
  • Fixed default debug rendering (defaults to console, view is optional)
  • Errors now have their own views determined by the JS error code or HTTP status code, with a catch-all default error view. These views are optional and have no matching controller or model. See readme for details.
  • Added example error views to scaffold

0.5.8

  • Small performance tweaks and debug additions in the server and router
  • Readme updates

0.5.7

  • Fixed a bug in the view directive when used within an include controller. Note that the view directive still can't (and never will) work within a cached include controller. You need to specify the include controller's view in the include directive of the calling controller if you intend to cache the result.
  • Improved error messages by throwing proper Error objects, allowing more useful trace logs
  • Cleaned up server.js and helpers.js a bit

0.5.6

  • Fixed a bug in view rendering caused by controllers that have includes but no content directive in the emitter
  • Improvements to format parameter handling

0.5.5

  • Bug fixes for JSON and JSONP output

0.5.4

  • Readme fixes related to cache syntax changes

0.5.3

  • Fixed a bug in https.secureCookies that prevented the cookie directive from overriding it. You can now tell citizen to intentionally set an insecure cookie in a secure environment.
  • Changed the default cookie path to urlPaths.app (cookies were being set to the root path "/" by default previously). This is technically a breaking change, but the worst that will happen is that cookies set prior to this change will no longer be accessible if your app path is something other than "/".

0.5.2

  • Added "secureCookies" option to "https" config. By default, all cookies set during an HTTPS request are secure. Setting this option to false allows non-secure cookies to be set by secure pages.

0.5.1

  • Forgot to update the config builder in util/scaffold.js with the new HTTP config

0.5.0

  • BREAKING CHANGE: Secure server support (HTTPS) has been added, resulting in minor changes to the way default hostnames and ports are stored in the config file. See "Configuration" and "HTTPS" in the readme for details.
  • BREAKING CHANGE (potentially): The default setting for citizen.urlPaths.app has been changed to "/" (previously an empty string). If you're referencing this variable within your own app to build URLs, it might cause problems.
  • Added a "path" option under "log" in the config so you can specify an alternate location for log files
  • Fixed a bug in error handling caused by the addition of helpers.public

0.4.1

  • Seems silly to change version numbers just for a readme fix, but that's the world we live in.

0.4.0

  • BREAKING CHANGE: Views rendered in a controller chain using handoff are now stored in the route.chain scope instead of the include scope (details in the readme under the "Controller Handoff" section)
  • BREAKING CHANGE: The syntax for the cache directive has been changed to make it a bit easier to understand. See the "Caching Routes and Controllers" section in the readme.
  • Fixed a bug in controller caching that threw an error when trying to cache a controller that also used the include directive
  • The "includeThisView" attribute within the handoff directive has been deprecated. If a controller in a handoff chain has a matching view, it's rendered automatically. If you leave this attribute in place, it won't break anything, but it will be ignored.
  • Added an error handler for EADDRNOTAVAIL at server startup (hostname unavailable/already in use)
  • Moved hasOwnProperty check in app.extend() to outer if statement so it covers both conditions
  • Added clearTimeout to session.end() so timers are cleared when a session is ended manually

0.3.8

  • app.copy() and app.extend() bug fixes and performance improvements
  • Made startup messaging consistent with actual settings

0.3.7

  • Fixed a bug in log()
  • app.config.hostname can be set to an empty string, allowing responses from any host
  • Switched object "deletions" to use null instead of undefined
  • Added config descriptions to readme
  • Removed index entry point (redundant)

0.3.6

  • Added another error scenario for HTTP port availability

0.3.5

  • Added httpPort to the skeleton app config because it's easier on new users to modify that setting if it's already there

0.3.4

  • Fixed a major bug in app.log() that pretty much broke logging entirely
  • Added a timestamp option to app.log() that lets you disable the timestamp in the log output
  • Added error handling for some common server startup problems and friendly messaging to aid in troubleshooting
  • Improved formatting for startup logs

0.3.3

  • Readme fixes

0.3.2

  • Added a util directory with a scaffolding CLI. You can now create an app skeleton and generate MVC patterns automagically. See the readme for instructions.

0.3.1

  • Readme fixes

0.3.0

  • BREAKING CHANGE: citizen includes are now self-contained and only play in their own sandbox. They no longer receive updated context from the calling controller, and they no longer pass their own content and directives into the request context. This just makes more sense and avoids a lot of pitfalls I was experiencing in my own projects.
  • BREAKING CHANGE: Completely rewrote caching, fixing bugs and making additions such as a custom scope option, allowing for easy deletion of groups of cached items. Please see the readme for changes to app.cache(), app.exists(), app.retrieve(), and app.clear(), which now all take options objects instead of a list of arguments.
  • BREAKING CHANGE: app.size() now throws an error if the provided argument isn't an object literal
  • Fixed several bugs in server.js that broke controller caching, route cache expiration, and cache URL params validation
  • Added app.log() helper to make it easier to log application events to the console or files based on application mode (production, debug, etc.)
  • Added prettyHTML config setting for Jade templates in production mode (default is true, setting it to false removes all whitespace between tags)
  • Fixed the default action in params.route.chain ('handler')
  • Rewrote app.dashes(), fixing a few bugs and adding a fallback option that gets returned if the parsed string ends up being empty

0.2.14

  • The ctznRedirect session variable now has a cookie fallback if sessions aren't enabled
  • Fixed a bug in direct cookie assignments

0.2.13

This is the beginning of the change log. I think citizen is "complete" enough to warrant it.

  • BREAKING CHANGE: Changed params.route.pathName to params.route.pathname
  • Enhanced redirect functionality