Mantra

Working Draft - Version 0.2.0

Introduction

This is a Draft Specification for Mantra, an application architecture for Meteor created by Kadira. It helps developers build maintainable, future‐proof Meteor apps.

Copyright notice

The MIT License (MIT)

Copyright (c) 2016 Kadira Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

  1. 1Overview
    1. 1.1What's Inside Mantra?
    2. 1.2What is Mantra Not?
    3. 1.3What is Mantra?
    4. 1.4Why a Spec?
    5. 1.5Before You Read
  2. 2Core Components
    1. 2.1Client-Side Focus
    2. 2.2ES2015 Syntax and ES2015 Modules
    3. 2.3React as the UI
    4. 2.4Actions
    5. 2.5State Management
    6. 2.6Containers
    7. 2.7Application Context
    8. 2.8Dependency Injection
      1. 2.8.1Configuring Dependency Injections
    9. 2.9Routing & Component Mounting
    10. 2.10Libraries
    11. 2.11Testing
      1. 2.11.1UI Testing
    12. 2.12Mantra Modules
      1. 2.12.1Application Context & Modules
      2. 2.12.2Module Definition
      3. 2.12.3Implicit Modules
      4. 2.12.4Module Containers & UI Components
      5. 2.12.5Module Actions
      6. 2.12.6Routes
      7. 2.12.7Core Module
      8. 2.12.8Sub Modules
    13. 2.13Single Entry Point
  3. 3Directory Layout
    1. 3.1Top-Level Directory Structure
      1. 3.1.1configs
      2. 3.1.2modules
        1. 3.1.2.1actions
        2. 3.1.2.2components
        3. 3.1.2.3containers
        4. 3.1.2.4configs
        5. 3.1.2.5libs
        6. 3.1.2.6routes.jsx
        7. 3.1.2.7index.js
      3. 3.1.3main.js
  4. 4Future Work
    1. 4.1Server-Side Rendering (SSR)
    2. 4.2Distributing Mantra Modules via NPM
    3. 4.3Standard for Styling
    4. 4.4Standard for Tests
    5. 4.5Reusing Composers
  5. 5Contributing to Mantra
  6. AAppendix: Prerequisite
    1. A.1ES2015
    2. A.2React
    3. A.3React Containers
    4. A.4Meteor Basics
  7. BAppendix: Server-Side Directory Layout
    1. B.1methods
      1. B.1.1Tests
    2. B.2publications
    3. B.3libs
    4. B.4configs
    5. B.5main.js
  8. CAppendix: Organizing Modules
    1. C.1Single Core Module
    2. C.2Core Module & Multiple Feature Modules
    3. C.3Multi Modules
    4. C.4Pages Module
  9. DAppendix: File Naming Conventions
    1. D.1Source File Names
    2. D.2Test File Names
      1. D.2.1Postfix

1Overview

Mantra is an application architecture for Meteor. With Mantra, we are trying to achieve two main goals.

1. Maintainability

Maintainability is a key factor for success when working with a large team. We achieve this by unit testing every part of our app, while following standards for everything. Then it’s easy to onboard new users and work with teams.

2. Future Proof

JavaScript is a land of choices. We have more than one best solution for each problem. It can be hard to tell what’s the best solution now and what will change in the future.

Mantra relies on a set of core principles that will last for a long time. Then, we allow others to change as needed.

1.1What's Inside Mantra?

1.2What is Mantra Not?

1.3What is Mantra?

1.4Why a Spec?

Mantra is an application architecture. There will be a lot of stakeholders for Mantra, including app developers, tool builders, tutorial authors, and project managers, so it’s very important to have a common standard everyone follows. That’s what this specification does.

1.5Before You Read

This specification is written in a simple language. However, you may feel more comfortable if you have a sound knowledge of the following areas.

Refer to Appendix A to learn more about the above areas.

2Core Components

Here are the core components of Mantra and how they are organized:

2.1Client-Side Focus

Mantra gives special attention to the client side of your app. Mantra does not mix client and server code together; instead, it recommends code sharing. Here are the reasons why:

Based on the above factors, it’s not a good idea to mix client and server code together.

When we discuss Mantra further in this specification, it will be about the client side of your app.

However, most of the apps will have server‐side components. So, we have a directory layout for the server side as well. For that, refer to Appendix B.

2.2ES2015 Syntax and ES2015 Modules

We rely on different features of ES2015 and its module system. In order to use Mantra, you need to use Meteor 1.3, which comes with an implementation of the ES2015 module system.

2.3React as the UI

We use React as the UI (presentation) layer in Mantra.

UI components should not know anything about the rest of the app and should not read or modify the application’s state. Data and event handlers used to render the UI component should be passed in via props from containers or passed in as action props from inside event handlers. It is sometimes necessary to use temporary local state inside a UI component, but that state should never be referenced outside of its own component.

When writing your UI components, you can include any other React component. Here are some places you can import React components:

You can also import any library function and use them in the UI components. You can import them directly from NPM modules, but not from any Meteor packages. These functions should be pure.

Here’s a simple UI component:

import React from 'react';

const PostList = ({posts}) => (
  <div className='postlist'>
    <ul>
      {posts.map(post => (
        <li key={post._id}>
          <a href={`/post/${post._id}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  </div>
);

export default PostList;

2.4Actions

Actions are where you write the business logic in your app. This includes:

An action is a simple function that accepts the first argument as the whole Application Context in your app. Other arguments usually come when invoking the action.

Inside an action, everything you do should be based on the Application Context and other arguments passed to the action. You should not import any ES2015 module except libraries. You should also avoid using Global variables inside actions.

Here are some actions:

export default {
  create({Meteor, LocalState, FlowRouter}, title, content) {
    if (!title || !content) {
      return LocalState.set('SAVING_ERROR', 'Title & Content are required!');
    }

    LocalState.set('SAVING_ERROR', null);

    const id = Meteor.uuid();
    // There is a method stub for this in the config/method_stubs
    // That's how we are doing latency compensation
    Meteor.call('posts.create', id, title, content, (err) => {
      if (err) {
        return LocalState.set('SAVING_ERROR', err.message);
      }
    });
    FlowRouter.go(`/post/${id}`);
  },

  clearErrors({LocalState}) {
    return LocalState.set('SAVING_ERROR', null);
  }
};

2.5State Management

In an app, we need to deal with different kinds of states. We can divide them into two different categories:

  1. Local State – State in the client‐side app that will never be synced with a remote server (errors, validation messages, current page).
  2. Remote State – This is the state usually fetched from a remote server and synced with it.

We have different solutions for managing states in our app, including:

This is where a lot of innovation is happening in the JavaScript community. So, Mantra is flexible when it comes to state management. You can use anything you want.

For example, you can use the following for your app when starting:

Later on you can move on to different solutions.

However, Mantra enforces a few rules when managing your states.

See the following links for some sample usage of states:

2.6Containers

Containers are the integration layer in Mantra. They perform these actions:

A container is a React component.

Containers are composed using react‐komposer. It supports different data sources, including Meteor/Tracker, Promises, Rx.js Observable, and nearly anything else.

Normally, inside a container you need to write the following functions:

We have some rules when creating a container:

If you need to pass the Application Context to a component, do it via props using a mapper.

Here’s an example container:

import PostList from '../components/postlist.jsx';
import {useDeps, composeWithTracker, composeAll} from 'mantra-core';

export const composer = ({context}, onData) => {
  const {Meteor, Collections} = context();
  if (Meteor.subscribe('posts.list').ready()) {
    const posts = Collections.Posts.find().fetch();
    onData(null, {posts});
  }
};

export default composeAll(
  composeWithTracker(composer),
  useDeps()
)(PostList);

2.7Application Context

Application Context is available to all actions and containers, so this is the place for shared variables in your app. These include:

Here’s a simple Application Context:

import * as Collections from '/lib/collections';
import {Meteor} from 'meteor/meteor';
import {FlowRouter} from 'meteor/kadira:flow-router';
import {ReactiveDict} from 'meteor/reactive-dict';
import {Tracker} from 'meteor/tracker';

export default function () {
  return {
    Meteor,
    FlowRouter,
    Collections,
    LocalState: new ReactiveDict(),
    Tracker
  };
}

2.8Dependency Injection

Mantra uses dependency injection to isolate different parts of your app including UI components and actions.

We use a project called react-simple-di that uses React Context behind the scenes. It accepts both Application Context and Actions as dependencies.

Once configured, Application Context will be injected into each action. That’s the first argument of an action. So, you don’t need to pass the application context manually.

Application Context can be accessed within Containers as well.

2.8.1Configuring Dependency Injections

Dependencies will be injected into the top‐level components in your app. Usually, it’ll be a Layout Component. You can do the injection inside your routes. See:

import React from 'react';
export default function (injectDeps) {
  // See: Injecting Deps
  const MainLayoutCtx = injectDeps(MainLayout);

  // Routes related code
}

2.9Routing & Component Mounting

When we refer to components, we consider both containers and UI components.

We normally use a Router to mount components to the UI. There could be multiple solutions (for example, Flow Router and React Router).

The Router’s only functionality in Mantra is to mount components to the UI. It’s just a tool.

See how to use FlowRouter as the router:

import React from 'react';
import {FlowRouter} from 'meteor/kadira:flow-router';
import {mount} from 'react-mounter';

import MainLayout from '/client/modules/core/components/main_layout.jsx';
import PostList from '/client/modules/core/containers/postlist';

export default function (injectDeps) {
  const MainLayoutCtx = injectDeps(MainLayout);

  FlowRouter.route('/', {
    name: 'posts.list',
    action() {
      mount(MainLayoutCtx, {
        content: () => (<PostList />)
      });
    }
  });
}
If you need to redirect upon some condition (for example user is not authorized) use an action instead of route options like FlowRouter’s triggersEnter. Call the action from component or container’s composer function.

2.10Libraries

Every app has some utility functions to do different tasks. You can also get them via NPM. These libraries will export functions. So, you can import them anywhere in your app including inside actions, components, and containers.

> When using a library function inside a component, it should be pure.

2.11Testing

Testing is a core part of Mantra. Mantra helps you test every part of your application. Rules we’ve enforced will help you write those tests. You can use familiar tools such as Mocha, Chai, and Sinon to perform testing.

With Mantra, you can unit test three core parts in your app. See:

2.11.1UI Testing

For the UI testing we use enzyme. Click here to see some sample test cases.

2.12Mantra Modules

Mantra follows a modular architecture. All the components of Mantra except “Application Context” should reside inside a module.

You can create as many as modules you like and communicate between them via imports.

2.12.1Application Context & Modules

Application Context is the core of the application. It needs to be defined in a place which does not belong to any module. All the modules can access Application Context as a dependency and modules should not update the Application Context.

2.12.2Module Definition

A Mantra module may contain a definition file. It exposes actions, routes, and a function accepting the context. It is the index.js file of a module.

A simple module definition looks like this:

export default {
  // optional
  load(context, actions) {
    // do any module initialization
  },
  // optional
  actions: {
    myNamespace: {
      doSomething: (context, arg1) => {}
    }
  },
  // optional
  routes(injectDeps) {
    const InjectedComp = injectDeps(MyComp);
    // load routes and put `InjectedComp` to the screen.
  }
};

2.12.3Implicit Modules

If the module has no actions or routes, or no need to do any initialization, then it’s okay to avoid using a definition file. These implicit modules may contain the following:

  • UI components
  • Containers
  • Libraries

2.12.4Module Containers & UI Components

Module containers and UI components should be able to be imported via ES2015 modules.

2.12.5Module Actions

A module can expose actions via namespaces. These namespaces are global to the app and the module should take responsibility for making them unique. A module can expose multiple namespaces.

Finally, all these namespaces from each module are merged and can be accessible inside actions and containers.

2.12.6Routes

For routing, you can use any routing library. It’s okay to have route definitions inside multiple modules if needed.

2.12.7Core Module

Mantra is 100% modular and there should be at least one in an app. We call this the core module. It’s just a simple module, but you load it before any other module. This module is the best place to put:

  • core routes,
  • application configurations,
  • common libraries,
  • common actions

and other application‐specific code.

There are multiple ways to organize modules depending on the app. Refer to Appendix C for some of those methods.

2.12.8Sub Modules

Inside a module, you cannot have sub modules. This is a decision made to prevent unnecessary complexity. Otherwise, it’s possible to write multiple layers of nested modules, and that’s very hard to manage.

2.13Single Entry Point

With Mantra, we want our app to be predictable. Therefore, there is a single entry point in your app. That’s the client/main.js.

It’ll initialize the Application Context and load all the modules in your app. Here’s an example client/main.js file:

import {createApp} from 'mantra-core';
import {initContext} from './configs/context';

// modules
import coreModule from './modules/core';
import commentsModule from './modules/comments';

// init context
const context = initContext();

// create app
const app = createApp(context);
app.loadModule(coreModule);
app.loadModule(commentsModule);
app.init();

3Directory Layout

In Mantra, we enforce a common directory structure. That’s the core part of the maintainability of any app.

In this section, we only discuss the client‐side directory layout. To learn about how to organize the server‐side directory layout, refer to Appendix B.

3.1Top-Level Directory Structure

All the Mantra‐related code stays inside the client directory of the app. Inside that, there are usually two directories and one JavaScript file. They are:

* configs
* modules
* main.js

Let’s have a look at each of these in detail.

3.1.1configs

This directory contains root‐level configurations in your app. Usually, this is a place to put app‐wide configurations which are common to all modules.

All JavaScript files in this directory should have a default export function which initiates some tasks and returns something if needed.

This is where we usually place Application Context as context.js.

3.1.2modules

This directory contains one or more modules (in their own directories) in your app. There should be at least a single module, and it’s usually named core.

This is what is usually inside a module directory:

* actions
* components
* configs
* containers
* libs
* routes.jsx
* index.js

Let’s learn more about these directories and files.

3.1.2.1actions

This directory contains all actions in the module. Here’s a sample directory layout inside it:

* posts.js
* index.js
* tests
    - posts.js

posts.js is an ES2015 module that exports a JavaScript object with actions. For example, here’s a simple action module:

export default {
  create({Meteor, LocalState, FlowRouter}, title, content) {
    //...
  },

  clearErrors({LocalState}) {
    //...
  }
};

Then, in the index.js, we import all the action modules and aggregate all actions. We give a namespace for each module.

import posts from './posts';

export default {
  posts
};

In the above example, we’ve given posts as the namespace for the posts.js action module.

These namespaces should be unique across the app. That’s a responsibility of the module.

In the tests directory, we write tests for each action module with its name. Refer to Appendix D to learn more about test file naming conventions.

Click here to see the directory layout for actions

3.1.2.2components

Components contain UI components of the module. It has a directory layout like this:

* main_layout.jsx
* post.jsx
* style.css
* tests
  - main_layout.js
  - post.js
  • All the .jsx files in this directory should have a default export. It should be a React class.
  • You can have CSS files related to these React components and Meteor will bundle them for you.

Just like in actions, we have a tests directory that contains tests. Refer to Appendix D for test file naming conventions.

Click here to see the directory layout for components.

3.1.2.3containers

This directory contains a set of .js files, with each of them representing a single container. Each file should have its default export as a React Container class.

Here’s a common directory layout:

* post.js
* postlist.js
* tests
    - post.js
    - postlist.js

This directory also has a tests directory which contain tests. Refer to Appendix D for test file naming conventions.

Click here to see the directory layout for containers.

3.1.2.4configs

This directory contains the module‐level configurations in your app.

All the JavaScript files in this directory must export a default function which initiates any task and returns something if needed. That function may accept “Application Context” as the first argument.

Here’s a simple config file:

export default function (context) {
  // do something
}

These configurations can be imported and called when loading the module.

Usually, this is where we keep Meteor method stubs which are used for optimistic updates.

Click here to see the directory layout for configs.

3.1.2.5libs

This directory contains a set of JavaScript files (.js or .jsx) which exports a set of utility functions. This is also known as libraries. You may write tests for libraries inside a subdirectory called tests.

3.1.2.6routes.jsx

This is the file containing route definitions of the module. It has a default export which is a function. This is a typical routes definition:

import React from 'react';
import {FlowRouter} from 'meteor/kadira:flow-router';
import {mount} from 'react-mounter';

import MainLayout from '/client/modules/core/components/main_layout.jsx';
import PostList from '/client/modules/core/containers/postlist';

export default function (injectDeps) {
  const MainLayoutCtx = injectDeps(MainLayout);

  FlowRouter.route('/', {
    name: 'posts.list',
    action() {
      mount(MainLayoutCtx, {
        content: () => (<PostList />)
      });
    }
  });
}

This default export is called with a function called injectDeps while loading the module. The injectDeps function can be used to inject dependencies into a React component (or a container) as shown above.

3.1.2.7index.js

This is the module definition file of the module. There is no need for this module definition file if there is no need to do any of the following tasks:

  • To load routes.
  • To define actions.
  • To run configurations while loading the module.

Here’s a typical module definition:

import methodStubs from './configs/method_stubs';
import actions from './actions';
import routes from './routes.jsx';

export default {
  routes,
  actions,
  load(context) {
    methodStubs(context);
  }
};

In the module definition, .load() method gets called when the module is loading. So, it’s the place to invoke configurations.

3.1.3main.js

This is the entry point of a Mantra app. It initializes the application context and loads modules. For that, it uses a utility library called mantra-core.

Here’s a sample main.js file:

import {createApp} from 'mantra-core';
import initContext from './configs/context';

// modules
import coreModule from './modules/core';
import commentsModule from './modules/comments';

// init context
const context = initContext();

// create app
const app = createApp(context);
app.loadModule(coreModule);
app.loadModule(commentsModule);
app.init();

4Future Work

Mantra is a draft and there will be missing pieces and improvements we can make. We’ve identified the following features as important to Mantra and they will be available in the near future.

4.1Server-Side Rendering (SSR)

It’s extremely possible to do SSR with Mantra. We are trying to do this in a tool‐agnostic manner, but the reference implementation will be based on FlowRouter SSR.

4.2Distributing Mantra Modules via NPM

We could distribute Mantra modules via NPM. Once we do that, we could do reuse a lot of code between apps and organizations.

4.3Standard for Styling

It’s better to have a standard for styling UI components.

4.4Standard for Tests

It’s better to have a standard for writing test cases.

4.5Reusing Composers

Sometimes, we can use reuse composers for the same function in many places. We need to find a pattern for doing that.

5Contributing to Mantra

This is a draft specification of Mantra.

To get started with Mantra, try following our sample app. It uses most of the features in Mantra.

We need your input for improving Mantra, so try using Mantra and let’s discuss how we can improve it.

AAppendix: Prerequisite

These resources will help you understand Mantra very clearly.

A.1ES2015

ES2015 is the standard version of JavaScript for 2015. It’s not fully implemented by all browsers or server‐side environments. But, using transpilers like babel, we can use E2015 today.

Meteor has built‐in support for ES2015

ES2015 is the best thing happen to JavaScript. It introduces a lot of features and fixes a lot of common issues.

A.2React

React is a UI framework based on JavaScript. Basically, you create the UI inside JavaScript. At first, it feels weird. But you’ll find it very interesting once you learn the basics.

Just forget about what you already know about HTML for a moment, and learn React. Then rethink. Here are some resources:

A.3React Containers

Now, we rarely use states in React components. Instead, we accept data via props. React’s stateless components make it very easy.

Then, we compose React containers to fetch data from different sources and load them into UI components. Projects like react‐komposer make it simple. Check out the following article for more information:

A.4Meteor Basics

You need to have a better understanding of Meteor. For that, follow Meteor’s official tutorial.

Mantra uses some of the above technologies a bit differently. For an example, Meteor’s React tutorial suggests using a mixin to access Mongo collection data. But Mantra uses a container, which is the modern way to use React.

BAppendix: Server-Side Directory Layout

This is a directory layout for the server side part of your app. This is not a core part of Mantra, but it follows the directory layout we used for the client side of our app.

On the server side, we have four main directories and a JavaScript file called main.js.

* methods
* publications
* libs
* configs
* main.js

Let’s see what each of these directories and files does.

B.1methods

This is the directory where we can put methods in your app. This is how the files in this directory look like:

* posts.js
* index.js
* tests
  - posts.js

Here we have a file called posts.js which has methods for the feature posts in our app. Depending your app, we can have different files.

Inside this JavaScript file, we have a default export which is a function. Meteor methods are defined inside that function.

When naming methods inside the posts.js, always prefix the method name. That prefix is the name of the file with a dot (.).

In our case, the prefix is posts.

For an example, here are some methods inside posts.js:

import {Posts, Comments} from '/lib/collections';
import {Meteor} from 'meteor/meteor';
import {check} from 'meteor/check';

export default function() {
  Meteor.methods({
    'posts.create'(_id, title, content) {
      //  method body
    }
  });

  Meteor.methods({
    'posts.createComment'(_id, postId, text) {
      //  method body
    }
  });
}

Finally, there is a file called index.js which imports all other modules in this directory and invokes them in a default export. So, when importing methods, we can do it with a single import.

Here’s a sample index.js file:

import posts from './posts';
import admin from './admin';

export default function () {
  posts();
  admin();
}

B.1.1Tests

We can write tests for methods inside the tests directory. For that, it’s a better to do integration testing rather doing unit tests.

For that, you can use Gagarin.

B.2publications

This directory is identical to the methods directory, but we write publications instead of methods.

B.3libs

This directory contains utility functions which we can use inside the server.

B.4configs

This is the place where we can write configurations in our app. These configuration files should have a default export function which can be imported and invoked. Configuration code should stay inside that function.

Here’s an example configuration:

export default function() {
  //  invoke the configuration here
}

B.5main.js

This is the place where we can start as the entry point for our app. We’ll import methods, publications and configuration inside this file and invoke.

Here’s an example main.js file:

import publications from './publications';
import methods from './methods';
import addInitialData from './configs/initial_adds.js';

publications();
methods();
addInitialData();
Have a look at this sample app to see how it has implemented these guidelines.

CAppendix: Organizing Modules

Mantra has a 100% module‐based app architecture. There should be at least a single module.

We’ve discussed how to organize files inside a module and how to use them. But, we didn’t discuss how to organize modules.

That’s because it’s different from app to app.

However, we are suggesting some potential patterns that can be used to organize modules.

C.1Single Core Module

For a simple app, we can put all the code inside a single module and name it as core. This would work for a simple app where there is a smaller client‐side codebase.

C.2Core Module & Multiple Feature Modules

This is an extended version of the above “Single Module App” pattern. Here it is:

C.3Multi Modules

This the multi‐module approach where there is no single core module.

C.4Pages Module

This can be used with any other pattern mentioned above.

Sometimes, we need to show some UI pages. They don’t have their own actions, routes, or configurations. They only contain some UI code. These can be either UI components or some containers.

For this purpose, we can use an implicit module.

DAppendix: File Naming Conventions

In Directory Layout, we discussed ways we can organize files for different components.

Here we discuss ways to name files.

D.1Source File Names

When we remove the extension from the filename it should satisfy following conditions:

Here’s the regular expression for the above rules:

/^[a-z]+[a-z0-9_]+$/

D.2Test File Names

This is how we name files inside the tests directory. Here are the rules:

D.2.1Postfix

Most of the time, we can have a test file for each source file. Sometimes, we need to create multiple test files for a single source file. That’s where we’ll use a postfix.

If that source filename is posts.js, then with the postfix it’ll look like this:

posts-part1.js
posts-part2.js

This is the regular expression for the above rules:

/^([a-z]+[a-z0-9_]+)(\-[a-z]+[a-z0-9_]+)*$/