Dependency injection Awilix 2 helpers and scope-instantiating middleware for Express. 🚀
npm install --save awilix-express
or
yarn add awilix-express
Requires Node v6 or above
Add the middleware to your Express app.
const { createContainer, Lifetime } = require('awilix');
const { scopePerRequest } = require('awilix-express');
const container = createContainer();
container.registerClass({
// Scoped lifetime = new instance per request
// Imagine the TodosService needs a `user`.
// class TodosService { constructor({ user }) { } }
todosService: [TodosService, Lifetime.SCOPED]
});
container.registerValue({ theAnswer: 42 });
// Add the middleware, passing it your Awilix container.
// This will attach a scoped container on the context.
app.use(scopePerRequest(container));
// Now you can add request-specific data to the scope.
app.use((req, res, next) => {
req.container.registerValue({
user: req.user // from some authentication middleware..
});
next();
});
Then in your route handlers...
const { inject } = require('awilix-express');
// `inject` accepts multiple parameters, not an array
router.get('/todos', inject('todosService', 'theAnswer'), (req, res) => {
req.todosService.find().then((result) => {
res.send({
result,
answer: req.theAnswer
});
});
});
You can also create invokers instead of using the inject
middleware:
// There's a makeClassInvoker for classes..
const { makeInvoker } = require('awilix-express');
function makeAPI ({ todosService }) {
return {
find: (req, res) => {
return todosService.find().then((result) => {
res.send(result);
});
}
};
}
const api = makeInvoker(makeAPI);
// Creates middleware that will invoke `makeAPI`
// for each request, giving you a scoped instance.
router.get('/todos', api('find'));
You can certainly use Awilix with Express without this library, but follow along and you might see why it's useful.
Imagine this simple imaginary Todos app, written in ES6:
// A totally framework-independent piece of application code.
// Nothing here is remotely associated with HTTP, Express or anything.
class TodosService {
constructor({ currentUser, db }) {
// We depend on the current user!
this.currentUser = currentUser;
this.db = db;
}
getTodos() {
// use your imagination ;)
return this.db('todos').where('user', this.currentUser.id);
}
}
// Here's a Express API that calls the service
class TodoAPI {
constructor({ todosService }) {
this.todosService = todosService;
}
getTodos(req, res) {
return this.todosService.getTodos().then(todos => res.send(todos));
}
}
So the problem with the above is that the TodosService
needs a currentUser
for it to function. Let's first try solving this manually, and then with awilix-express
.
This is how you would have to do it without Awilix at all.
import db from './db'
router.get('/todos', (req, res) => {
// We need a new instance for each request,
// else the currentUser trick wont work.
const api = new TodoAPI({
todosService: new TodosService({
db,
// current user is request specific.
currentUser: req.user
});
});
// invoke the method.
return api.getTodos(req, res);
});
Let's do this with Awilix instead. We'll need a bit of setup code.
import { createContainer, Lifetime } from 'awilix';
const container = createContainer();
// The `TodosService` lives in services/TodosService
container.loadModules(['services/*.js'], {
// we want `TodosService` to be registered as `todosService`.
formatName: 'camelCase',
registrationOptions: {
// We want instances to be scoped to the Express request.
// We need to set that up.
lifetime: Lifetime.SCOPED
}
});
// imagination is a wonderful thing.
app.use(someAuthenticationMethod());
// We need a middleware to create a scope per request.
// Hint: that's the scopePerRequest middleware in `awilix-express` ;)
app.use((req, res, next) => {
// We want a new scope for each request!
req.container = container.createScope();
// The `TodosService` needs `currentUser`
req.container.registerValue({
currentUser: req.user // from auth middleware.. IMAGINATION!! :D
});
return next();
});
Okay! Let's try setting up that API again!
export default function (router) {
router.get('/todos', (req, res) => {
// We have our scope available!
const api = new TodoAPI(req.container.cradle); // Awilix magic!
return api.getTodos(req, res);
});
}
A lot cleaner, but we can make this even shorter!
export default function (router) {
// Just invoke `api` with the method name and
// you've got yourself a middleware that instantiates
// the API and calls the method.
const api = (methodName) => {
// create our handler
return function (req, res) {
const controller = new TodoAPI(req.container.cradle);
return controller[method](req, res);
};
};
// adding more routes is way easier!
router.get('/todos', api('getTodos'));
}
In our route handler, do the following:
import { makeClassInvoker } from 'awilix-express';
export default function (router) {
const api = makeClassInvoker(TodoAPI);
router.get('/todos', api('getTodos'));
};
And in your Express application setup:
import { createContainer, Lifetime } from 'awilix';
import { scopePerRequest } from 'awilix-express';
const container = createContainer();
// The `TodosService` lives in services/TodosService
container.loadModules([
['services/*.js', Lifetime.SCOPED] // shortcut to make all services scoped
], {
// we want `TodosService` to be registered as `todosService`.
formatName: 'camelCase'
});
// imagination is a wonderful thing.
app.use(someAuthenticationMethod());
// Woah!
app.use(scopePerRequest(container));
app.use((req, res, next) => {
// We still want to register the user!
// req.container is a scope!
req.container.registerValue({
currentUser: req.user // from auth middleware.. IMAGINATION!! :D
});
next();
});
Now that is way simpler! If you are more of a factory-function aficionado like myself, you can use makeInvoker
in place of makeClassInvoker
:
import { makeInvoker } from 'awilix-express';
function makeTodoAPI ({ todosService }) {
return {
getTodos: (req, res) => {
return todosService.getTodos().then((todos) => res.send(todos));
}
};
}
export default function (router) {
const api = makeInvoker(makeTodoAPI);
router.get('/api/todos', api('getTodos'));
};
That concludes the tutorial! Hope you find it useful, I know I have.
npm test
: Runs tests oncenpm test -- --watch
: Runs tests in watch-modenpm run lint
: Lints the code oncenpm run coverage
: Runs code coverage usingistanbul
Talysson - @talyssonoc