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.
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.
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.
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.
Here are the core components of Mantra and how they are organized:
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.
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.
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;
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.
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);
}
};
In an app, we need to deal with different kinds of states. We can divide them into two different categories:
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.
See the following links for some sample usage of states:
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:
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);
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
};
}
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.
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
}
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 />)
});
}
});
}
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.
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:
For the UI testing we use enzyme. Click here to see some sample test cases.
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.
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.
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.
}
};
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:
Module containers and UI components should be able to be imported via ES2015 modules.
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.
For routing, you can use any routing library. It’s okay to have route definitions inside multiple modules if needed.
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:
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.
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.
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();
In Mantra, we enforce a common directory structure. That’s the core part of the maintainability of any app.
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.
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.
context.js
.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.
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.
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.
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
.jsx
files in this directory should have a default export. It should be a React class.Just like in actions, we have a tests directory that contains tests. Refer to Appendix D for test file naming conventions.
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.
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.
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
.
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.
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:
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.
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();
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.
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.
We could distribute Mantra modules via NPM. Once we do that, we could do reuse a lot of code between apps and organizations.
It’s better to have a standard for styling UI components.
It’s better to have a standard for writing test cases.
Sometimes, we can use reuse composers for the same function in many places. We need to find a pattern for doing that.
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.
These resources will help you understand Mantra very clearly.
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.
ES2015 is the best thing happen to JavaScript. It introduces a lot of features and fixes a lot of common issues.
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:
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:
You need to have a better understanding of Meteor. For that, follow Meteor’s official tutorial.
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.
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();
}
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.
This directory is identical to the methods
directory, but we write publications instead of methods.
This directory contains utility functions which we can use inside the server.
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
}
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();
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.
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.
This is an extended version of the above “Single Module App” pattern. Here it is:
This the multi‐module approach where there is no single core module.
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.
In Directory Layout, we discussed ways we can organize files for different components.
Here we discuss ways to name files.
When we remove the extension from the filename it should satisfy following conditions:
_
symbol.Here’s the regular expression for the above rules:
/^[a-z]+[a-z0-9_]+$/
This is how we name files inside the tests
directory. Here are the rules:
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_]+)*$/