Rafter is a lightweight, slightly opinionated Javascript framework for rapid development of web applications.
- is built using Typescript.
- is built on top of Expressjs.
- eliminates the tedious wiring of routes, middleware and services.
- allows decoupling of services by utilizing dependency injection via the autoloading service container Awilix.
- is flexible, reusable and testable.
yarn add rafter
yarn bootstrap & yarn build & yarn start:boilerplate
This will build and run the boilerplate project. You can access it via http://localhost:3000.
Dependency autoloading is at the heart of Rafter, and the most opinionated portion of the framework. Rafter utilizes Awilix under the hood to automatically scan your application directory and register services. So there's no need to maintain configuration or add annotations, as long as the function or constructor arguments are the same name, it will wire everything up automatically.
Logger.ts
class Logger implements ILogger {
public log(...args: any[]): void {
console.log(args);
}
}
MyService.ts
class MyService {
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
public run(): void {
this.logger.log('I have been autowired');
}
}
The Rafter autoloader will look recursively throughout your project for services, functions and config. This means you do not need to statically import
all your dependencies, which maintains separation of concerns and improves reusability.
The following configuration files are autoloaded by Rafter.
config.ts
: a general application or module config.middleware.js
: registers services as middleware and loads them into the routes stack.routes.ts
: links controller services to route definitions.pre-start-hooks.js
: loads defined services before Rafter has started the server.
The config file (config.ts
) is a place to define all your application style config.
export default () => ({
db: {
connection: 'mongodb://localhost:27000/rafter' || process.env.NODE_DB_CONNECTION,
},
server: {
port: 3000,
},
example: {
message: `Hello Mars`,
},
});
This config can be referenced within the injected dependencies.
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 (): IMiddlewareConfig[] => [`corsMiddleware`, `authenticationMiddleware`];
The routes file (routes.ts
) exports an array of objects which define the http method, route, controller and action. eg.
export default (): IRouteConfig[] => [
{
endpoint: `/`,
controller: `exampleController`,
action: `index`,
method: `get`,
},
];
This would call exampleController.index(req, res)
when the route GET /
is hit. exampleController
will be the name of the autoloaded service.
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 (): IPreStartHookConfig[] => [`connectDbService`];
An example of the connectDbService
pre start hook would be:
export default (dbDao: IDBDatabaseDao, logger: ILogger) => {
return async function connect(): Promise<IDbConnection> {
logger.info(`Connecting to the database`);
return dbDao.connect();
};
};
By adding async
to the function, Rafter will wait for it to be successfully returned before continuing to the next pre start hook, or will finish starting up if there are no more hooks.
Along with the aforementioned configs, all that is required to run Rafter is the following in an index.ts
file:
import rafter from 'rafter';
const run = async () => {
// define the paths you want to autoload
const paths = [join(__dirname, '/**/!(*.spec).@(ts|js)')];
// instantiate rafter
const rafterServer = rafter({ paths });
// start rafter server
await rafterServer.start();
};
run();
Once start()
is called, Rafter will:
- Scan through all your directories looking for config files and services.
- 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, or look at the included boilerplate
application within packages.
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
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.
import mongoose from 'mongoose';
const connect = async connectionUrl => {
await mongoose.connect(connectionUrl);
};
const find = async query => {
await mongoose.find(query);
};
export { connect };
export class DbDao {
private db: IDatabaseDao;
private config: {connectionUrl: string};
constructor(db: IDatabaseDao, config: {connectionUrl: string}) {
this.db = db;
this.config = config;
}
public async connect(): Promise<IDatabaseConnection> {
return this.db.connect(this.config.connectionUrl);
}
public async find<T>(query: any): Promise<T> {
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.