npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

citizen

v1.0.1

Published

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

Downloads

227

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 are numerous breaking changes in the 1.0.x release. Please consult the changelog for an itemized list and review this updated documentation thoroughly.

Benefits

  • High performance and stability
  • 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: {
      enable: false
    },
    https: {
      enable: 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 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

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.

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. 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/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": "[email protected]"
  }
}

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": "[email protected]"
  }
});

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 {
    let err = new Error('Not found.')
    // The HTTP status code defaults to 500, but you can specify your own
    err.statusCode = 404
    err.message = 'The requested article does not exist.'
    throw err
  }
}

Note that params.route.controller is updated from the requested controller to error if you reference it within your app.

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.

Capture vs. Exit

citizen's default error config is capture, which attempts graceful recovery. If you'd prefer the process exit 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 ccnst 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": {
      "control