rafter
v0.8.75
Published
A slightly opinionated JS framework that makes building apps easy and testable
Downloads
1,116
Readme
Rafter Framework
Rafter is a lightweight, slightly opinionated Javascript framework for rapid development of web applications.
Rafter:
- is built on top of Expressjs.
- eliminates the tedious wiring of routes, middleware and services.
- allows decoupling of services by utilizing dependency injection via an autoloading service container.
- is flexible and testable.
Install
npm install --save rafter
OR
yarn add rafter
Getting started
The following configuration files are autoloaded during the Rafter service starting:
config.ts
: a general application or module config.middleware.js
: registers services as middleware.routes.ts
: links controller services to route definitions.pre-start-hooks.js
: loads defined services before Rafter has started the server.
The Rafter autoloader will look for all of these files recursively throughout your project. This allows you to modularize your project rather than defining all your config in one place.
Config
The config file (config.ts
) is a place to define all your application style config.
export default {
db: {
connectionUrl: 'mongodb://localhost:27000/rafter' || process.env.NODE_DB_CONNECTION,
},
server: {
port: 3000,
},
example: {
message: `Hello Mars`,
},
};
Middleware
The middleware file (middleware.js
) exports an array of service name references which will be loaded/registered in the order in which they were defined. eg.
export default (): IMiddlewares => new Set<IMiddlewareConfig>([`corsMiddleware`, `authenticationMiddleware`]);
Note; the middleware must be registered in the .services.ts
config.
Routes
The routes file (routes.ts
) exports an array of objects which define the http method, route, controller and action. eg.
export default (): IRoutes =>
new Set<IRouteConfig>([
{
endpoint: `/`,
controller: `exampleController`,
action: `index`,
method: `get`,
},
]);
This would call exampleController.index(req, res)
when the route GET /
is hit.
Pre start hooks
The routes file (pre-start-hooks.js
) exports an array of service references that will be executed before Rafter has started, in the order in which they were defined. This is useful for instantiating DB connections, logging etc.
export default (): IPreStartHooks => new Set<IPreStartHookConfig>([`connectDbService`]);
An example of the connectDbService
would be:
export default (dbDao, logger) => {
return async () => {
logger.info(`Connecting to the database`);
return dbDao.connect();
};
};
Rafter instantiation
Along with the aforementioned configs, all that is required to run Rafter is the following:
import rafter from 'rafter';
const run = async () => {
const rafterServer = rafter();
await rafterServer.start();
};
run();
Once start()
is called, Rafter will:
- Scan through all your directories looking for config files.
- Autoload all your services into the service container.
- Run all the
pre-start-hooks
. - Apply all the middleware.
- Register all the routes.
- Start the server.
To see an example project, visit the skeleton rafter app repository.
Going deeper
Rafter is slightly opinionated; which means we have outlined specific ways of doing some things. Not as much as say, Sails or Ruby on Rails, but just enough to provide a simple and fast foundation for your project.
The foundations of the Rafter framework are:
- Dependency injection
- Autoloading services
- Configuration
Dependency injection
With the advent of RequireJs
, dependency injection (DI) had largely been thrown by the way side in favor of requiring / importing all your dependencies in Node. This meant that your dependencies were hard coded in each file, resulting in code that was not easily unit testable, nor replicable without rewrites.
eg.
With RequireJs
import mongoose from 'mongoose';
const connect = async (connectionUrl) => {
await mongoose.connect(connectionUrl);
};
const find = async (query) => {
await mongoose.find(query);
};
export { connect };
With DI
export default class DbDao {
constructor(db) {
this._db = db;
}
async connect(connectionUrl) {
return this._db.connect(connectionUrl);
}
async find(query) {
return this._db.find(query);
}
}
As you can see with DI, we can substitute any DB service rather than being stuck with mongoose. This insulates services which use a data store from caring what particular store it is. eg. If our DB becomes slow, we can simply substitute a CacheDao
instead, and no other services would have to change.