-
Notifications
You must be signed in to change notification settings - Fork 7
Sub Applications
Each part of the application is broken down into a 'sub-app'. A sub-app is a mini semi-self contained express application that
Each sub-app needs to provide an index.js what provide at least 3 pieces of information:
- displayName: The name of the sub-app to be shown into level navigation, headings and breadcrumbs
- mountpath: The top level URL to mount the sub-app under, e.g. '/contacts'
- router: An router object, described later
# src/apps/companies/index.js
const router = require('./router')
module.exports = {
displayName: 'Companies',
mountpath: '/companies',
router,
}
In some cases, you may want to define a sub-app that only contains logic or code tied to an entity but that does not have web pages associated with it. The 'Adviser' sub-app is one such app that only contains repository and transformer code, so it has an empty index file to indicate to the bootloader that it should not be added to the routes like the others.
# src/apps/adviser/index.js
module.exports = {}
Each sub app breaks down its functionality into a number of key pieces of functionality. The core of a sub-app is its router, this defines how requests are handled. The rest of the files follow the model used throughout the application. The main ideology behind the file structure is the use of small distinct middleware functions to carry out a piece of work and ultimately a controller that will call the render function to generate HTML. In addition to middleware and controllers, other functions such as 'repositories', 'services' and transformers are used to gather and manipulate data. The full list of sub-app elements are:
-
Router: The router is the nerve centre of any sub-app, it defined how requests are handled and how the different parts of the sub-app communicate with each other. If a developer is ever unsure how a request is handled or how sub-app works then the router file is the first place to look. The router file is always named
router.js
. - Controllers: Small and lightweight. In a simple operation where a piece of data is required, the controller may call the repository to fetch it and then pass it to the render layer in Nunjucks. Anything more complex than this is split out into middleware.
- Repos (Repository): A repository contains a collection of functions to fetch or save data, they contain no business logic but are primarily charged with generating the parameters needed to be sent to the API. Once data has been fetched they return the raw data.
-
Transformers: A transformer is responsible for taking data in one format and turning it into another. Transformers are used heavily throughout the application with two main type:
- List item transformer: Typically used to describe a single item in a search result so it can be rendered on screen.
- Detail record: This would transform a single record into a format for use in a detail view
- Form: Some data may need transforming from the API format into a format that can work with the form builders and vice versa. Some sub-apps have this in a
services
file which dates back to a previous naming convention, but as they are turning one format of data into another they are technically transformers.
- Macros: Macros are not really macros but actually define the structure of forms, both for data editing and other interface elements such as filter forms. A macro defines the form structure, the interface components to use when rendering and are used in conjunction with the form builder functions to combine they definition with state and errors.
- Middleware: Middleware is functions that are used to carry out a task in the chain of things called when a request is made. Typically a middleware would carry out tasks such as coordinating the fetching and transformation of data then passing it onto the control to render.
- Views: Each sub app will contain views that are unique to that sub app, which in turn call upon common layouts and macros.
- Labels: Labels used by forms and other components should be kept in a central file for each sub-app.
Other files often seen in a sub-app may include constants.js
or services
. Services are based on an older file naming convention and typically can be better split into middleware or transformers.
The application names and conventions are evolved over time so some sub-app may have variations on these names.
Sub apps follow a convention to help keep finding things easy and predictable, whilst allowing code to scale from small sub-apps to complex sub-apps with many elements.
All sub-app files and folders are named using the plural of the subject they address, e.g. controllers. For something with only one or two functions a single file can be used that exports the functions required.
# src/apps/contacts/repos.js
function getContact (token, contactId) {
return authorisedRequest(token, `${config.apiRoot}/v3/contact/${contactId}`)
}
module.exports = {
getContact,
}
Once the code grows to a few more functions, and as such grows in size and its corresponding unit test files grow even bigger, it makes sense to split the single file into multiple. In this case, a folder is created with the name and within that folder separate files are used for each function or subject. The level of granularity will depend upon the code complexity, typically at first, you could start with related functions that handle a thing such as 'collections' and then if that grows more complex then break it down into a file per function. All the functions in a folder are then exported via an index.js file, this way other modules that depend upon the module being refactored do not have to be changed; this makes re-factoring less painful.
# src/apps/interactions/middleware/index.js (yes that's the plural)
const {
getInteractionCollection,
getInteractionsRequestBody,
getInteractionSortForm,
} = require('./collection.js')
const {
getInteractionDetails,
postDetails,
getInteractionOptions,
} = require('./details.js)
module.exports = {
getInteractionCollection,
getInteractionsRequestBody,
getInteractionSortForm,
getInteractionDetails,
postDetails,
getInteractionOptions,
}
--------------------------------------------------
# src/apps/interactions/middleware/collection.js
async function getInteractionCollection (req, res, next) {
...
}
module.exports = {
getInteractionDetails,
postDetails,
getInteractionOptions,
}
--------------------------------------------------
# src/apps/router.js
const { getInteractionCollection } = require('./middleware)
All file names use lower case and kebab case, e.g. my-function.js
. The files for each sub-app follow set names to make it easier to find the required file, and when naming files try to look for other similar uses to aid in that consistency. Most developers use an editor that allows fast navigation to a file based on its name and it can make their life much easier.
The heart of a sub-app is it's routing, this is where the param handling, middleware and controllers are configured.
The router file works like any other Express JS routing file, it starts off with getting a router from Express.
const router = require('express').Router()
This is followed by pulling in all the required functions from the sub-app.
In some sub apps, you may need to start off with a middleware to cover the entire sub-app to check permissions for it or you may need to check permissions on a single route. The permission details themselves are normally stored in constants.js
. Permissions are a complex enough subject to discuss elsewhere.
# Check single route
router.use('/:contactId', handleRoutePermissions(LOCAL_NAV), getCommon, setLocalNav(LOCAL_NAV))
# Check permissions for any request
router.use(handleRoutePermissions(APP_PERMISSIONS))
A common task for a sub-app is to retrieve an entity for a given id, so in most router files one of the first routes set-up is a param
handler. This names a function to be called for any call to a route with a predefined parameter name named after the entity that is the subject of the sub-app. For companies, you would create a parameter named companyId
then add a handler to the router.
router.param('companyId', getCompany)
The rest of the route file contains routes for the different actions and views. When writing new routes try and break down the functions so each function does one part of a journey, such as handle a form post. Balance the granularity of middleware/controllers with code that is easy to manage. Don't create lost of very small middleware which results in a complex route, but do create middleware that can be shared or logically split up the task at hand. Focussed middleware is easier to test.
The form builder and handling documentation give more detail about the idea of having a form handler middleware fail through to a rendering controller. Collection routes typically split out the jobs of handling parameters, fetching/transforming data and rendering the output.