Jimpex
Express as dependency injection container.
Jimpex is an implementation of Express, one of the most popular web frameworks for Node, using Jimple, a Javascript port of Pimple dependency injection container.
Motivation/Introduction
A friend who's also web developer brought the idea of start using a dependency injection container on Node, and how Jimple was a great tool for it, and from that moment on I can't think of starting an app without using it. It not only allows you to implement dependency injection on a simple and clean way but it also kind of forces you to have a really good organization of your code.
A couple of months after that, the same friend told me that we should do something similar to Silex, which is based on Pimple, with Express. I ran with the idea and... this project is what I think a mix of Jimple and Express would look like. To be clear, this is not a port of Silex.
Information
- | - |
---|
Package | jimpex |
Description | Express as dependency injection container. |
Node Version | >= v8.0.0 |
Usage
Creating your app
To create a Jimpex app you would require the Jimpex
class from the package, extend it and define all your services, controllers and middlewares on its boot
method:
const { Jimpex } = require('jimpex');
class MyApp extends Jimpex {
boot() {
}
}
The class constructor has two parameters:
boot
(true
): Whether or not to call the boot
method after initializing the instance.options
({}
): A set of options to customize the app.
There are a lot of options to customize an app, so I would recommend you to read the Jimpex Options documentation.
App configuration
Jimpex, by default, depends on external configuration files and as a base configuration it will try to load ./config/app/app.config.js
. Of course this is extremely configurable through the Jimpex Options.
A configuration file is just a Javascript file that exports an Object, for example:
module.exports = {
port: 2509,
};
If that's who you default configuration file looks like, the app will run on the port 2509
.
To access the app configuration, you just call the appConfiguration
service:
const config = app.get('appConfiguration');
Then you can read its values using .get(setting)
:
console.log(config.get('port'));
To more information about how the appConfiguration
service works, you can check its documentation on the wootils repository.
Starting the app
To start the app you need a valid configuration file with a valid port
setting. Check the previous section to more information about it.
Now, Starting the app is as easy as calling start()
:
app.start(() => {
console.log('The app is running!');
});
- Like Express, you can send a callback to be executed after the server starts.
- You also have a
listen
alias with the same signature as express (port and callback) for serverless platforms where you don't manually start the app.
You can also stop the app by calling stop()
:
app.stop();
Defining a service
To define a service and its provider, you would write your service as a class
or a function
and then wrap it on the provider
function Jimpex provides:
const { provider } = require('jimpex');
class MyService {
constructor(depOne, depTwo);
}
const myService = provider((app) => {
app.set('myService', () => new MyService(
app.get('depOne'),
app.get('depTwo')
));
});
module.exports = {
MyService,
myService,
};
You could export just export the provider, but I believe is a good practice to export both in case another part of your app wants to extend the class and overwrite the service on the container.
Then, on you app, you would simple register
the provider:
const { Jimpex } = require('jimpex');
const { myService } = require('...');
class MyApp extends Jimpex {
boot() {
...
this.register(myService);
}
}
Done, your service is now available.
Defining a configurable service
In case you want to create a service that could accept custom setting when instantiated, you can use a "provider creator":
const { providerCreator } = require('jimpex');
class MyService {
constructor(depOne, depTwo, options = {});
}
const myService = providerCreator((options) => (app) => {
app.set('myService', () => new MyService(
app.get('depOne'),
app.get('depTwo'),
settings
));
});
module.exports = {
MyService,
myService,
};
The special behavior the creators have, is that you can call them as a function, sending the settings, or just use them on the register
, so it's very important that the settings must be optional:
const { Jimpex } = require('jimpex');
const { myService } = require('...');
class MyApp extends Jimpex {
boot() {
...
this.register(myService);
this.register(myService({ ... }));
}
}
Adding a controller
To add controller you need to use the controller
function and return a list of routes:
const { controller } = require('jimpex');
class HealthController {
health() {
return (req, res) => {
res.write('Everything works!');
};
}
}
const healthController = controller((app) => {
const ctrl = new HealthController();
const router = app.get('router');
return [
router.get('/', ctrl.health()),
];
});
module.exports = {
HealthController,
healthController,
};
- You could export just export the controller, but I believe is a good practice to export both in case another part of your app wants to extend the class and mount a new route withs its inherit functionalities.
- The function inside the
controller
wrapper won't be called until the app is started. In case you are wondering about the lazy loading of the services that you may inject.
Then, on you app, you would mount
the controller:
const { Jimpex } = require('jimpex');
const { healthController } = require('...');
class MyApp extends Jimpex {
boot() {
...
this.mount('/health', healthController);
}
}
Defining a configurable controller
Like with _"providers creators", you can define controllers that accept custom settings when
instantiated, using a "controller creator":
const { controllerCreator } = require('jimpex');
class HealthController {
constructor(settings = {});
health() {
return (req, res) => {
res.write('Everything works!');
};
}
}
const healthController = controllerCreator((settings) => (app) => {
const ctrl = new HealthController(settings);
const router = app.get('router');
return [
router.get('/', ctrl.health()),
];
});
module.exports = {
HealthController,
healthController,
};
The special behavior the creators have, is that you can call them as a function, sending the settings, or just use them with mount
as regular controllers; and since they can be used as regular controllers, it's very important that the settings are optional:
const { Jimpex } = require('jimpex');
const { healthController } = require('...');
class MyApp extends Jimpex {
boot() {
...
this.mount('/health', healthController);
this.mount('/health', healthController({ ... }));
}
}
Adding a middleware
To add a new middleware you need to use the middleware
function and return a function:
const { middlware } = require('jimpex');
const greetingsMiddleware = () => (req, res, next) => {
console.log('Hello!');
};
const greetings = middleware(() => greetingsMiddleware());
module.exports = {
greetingsMiddleware,
greetings,
};
You could export just export the provider, but I believe is a good practice to export both in case another part of your app wants to extend the class or use the function.
Then, on you app, you would use
the controller:
const { Jimpex } = require('jimpex');
const { greetings } = require('...');
class MyApp extends Jimpex {
boot() {
...
this.use(greetings);
}
}
Defining a configurable middleware
Like with controllers and providers, you can also create a middleware that can accept settings when instantiated, with a "middleware creator":
const { middlwareCreator } = require('jimpex');
const greetingsMiddleware = (message = 'Hello!') => (req, res, next) => {
console.log(message);
};
const greetings = middlewareCreator((message) => greetingsMiddleware(message));
module.exports = {
greetingsMiddleware,
greetings,
};
The special behavior the creators have, is that you can call them as a function, sending the settings, or just register them with use
as regular middlewares, so it's very important that the settings must be optional:
const { Jimpex } = require('jimpex');
const { greetings } = require('...');
class MyApp extends Jimpex {
boot() {
...
this.use(greetings);
this.use(greetings('Howdy!'));
}
}
Built-in features
Jimpex comes with a few services, middlewares and controllers that you can import and use on your app, some of them are activated by default on the options, but others you have to implement manually:
Controllers
- Configuration: Allows you to see and switch the current configuration. It can be enabled or disabled by using a setting on the configuration.
- Health: Shows the version and name of the configuration, just to check the app is running.
- Statics: It allows your app to server specific files from any directory, without having to use the
static
middleware. - Gateway: It allows you to automatically generate a set of routes that will make gateway requests to an specific API.
Read more about the built-in controllers
Middlewares
- Error handler: Allows you to generate responses for errors and potentially hide uncaught exceptions under a generic message, unless it's disabled via configuration settings.
- Force HTTPS: Redirect all incoming traffic from HTTP to HTTPS. It also allows you to set routes to ignore the redirection.
- Fast HTML: Allows your app to skip unnecessary processing by showing an specific HTML when a requested route doesn't have a controller for it or is not on a "whitelist".
- Show HTML: A really simple middleware to serve an HTML file. Its true feature is that it can be hooked up to the HTML Generator service.
- Version validator: If you mount it on a route it will generate a
409
error if the request doesn't have a version parameter with the same version as the one on the configuration.
Read more about the built-in controllers
Services
- API client: An implementation of the wootils API Client but that is connected to the HTTP service, to allow logging and forwarding of the headers.
- App Error: A very simple subclass of
Error
but with support for context information. It can be used to customize the error handler responses. - Ensure bearer token: A service-middleware that allows you to validate and retrieve a bearer token from the incoming requests
Authorization
header. - HTTP Error: Another type of error, but specific for the HTTP requests the app does with the API client.
- Send File: It allows you to send a file on a response with a path relative to the app executable.
- Frontend Fs: Useful for when your app has a bundled frontend, it allows you to read, write and delete files with paths relative to the app executable.
- HTML Generator: A service that allows you to generate an HTML file when the app gets started and inject contents of the configuration as a
window
variable. - HTTP: A set of utilities to work with HTTP requests and responses.
- Responses builder: A service that generates JSON and HTML responses.
Read more about the built-in services
The service also implements a few other services from the wootils as core utilities:
appLogger
: The logger service.environmentUtils
: The service that reads the environment variables.packageInfo
: The app package.json information.pathUtils
: The service to build paths relative to the project root directory.rootRequire
: The service to make requires relatives to the project root directory.events
: To handle the app events.
Development
Before doing anything, install the repository hooks:
yarn run hooks
NPM/Yarn Tasks
Task | Description |
---|
yarn run hooks | Install the GIT repository hooks. |
yarn test | Run the project unit tests. |
yarn run lint | Lint the modified files. |
yarn run lint:full | Lint the project code. |
yarn run docs | Generate the project documentation. |
yarn run todo | List all the pending to-do's. |
Testing
I use Jest with Jest-Ex to test the project. The configuration file is on ./.jestrc
, the tests and mocks are on ./tests
and the script that runs it is on ./utils/scripts/test
.
Linting
I use ESlint to validate all our JS code. The configuration file for the project code is on ./.eslintrc
and for the tests on ./tests/.eslintrc
(which inherits from the one on the root), there's also an ./.eslintignore
to ignore some files on the process, and the script that runs it is on ./utils/scripts/lint
.
Documentation
I use ESDoc to generate HTML documentation for the project. The configuration file is on ./.esdocrc
and the script that runs it is on ./utils/scripts/docs
.
To-Dos
I use @todo
comments to write all the pending improvements and fixes, and Leasot to generate a report. The script that runs it is on ./utils/scripts/todo
.