kojo
v8.2.1
Published
An event-driven microservice framework. Kōjō (工場) means 'plant' in Japanese.
Downloads
95
Maintainers
Readme
🏭 Kojo
An event-driven microservice framework. Kōjō (工場) means 'factory' in Japanese.
The idea of this framework emerged after a couple of years of using Seneca, which in turn is a great tool for building microservices but probably wants to be too abstract.
Kojo, on the other hand, is very straightforward: it has subscribers (or routes, or endpoints), services and methods which are just plain functions. Subscribers susbscribe to a pub/sub (or request/response, or a schedule) transport of your choice and call services while services perform various tasks via their methods.
Installation
npm i kojo
Usage
NOTE: Starting from v8.0.0 this package moved to native ESM modules.
Create a service with a method (services/user/create.js
):
export default async function (userData) {
const [ kojo, logger ] = this; // kojo instance and the logger
const { pg: pool } = kojo.state; // get previously set pg connection
logger.debug('creating', userData); // logger will automatically add module and method name
const query = `INSERT INTO ... RETURNING *`;
const result = await pool.query(query);
const newRecord = result ? result.rows[0] : null;
if (newRecord)
logger.info('created', newRecord);
return newRecord;
}
Create a subscriber (subscribers/user.create.js
):
export default (kojo, logger) => {
const { user } = kojo.services; // we defined `user` service above
const { nats } = kojo.state; // as with pg connection above we have nats connection too
nats.subscribe('user.create', async (userData) => {
const newUser = await user.create(userData);
if (newUser)
nats.publish('user.created', newUser);
});
}
Add connections, initialize kojo:
...
import Kojo from 'kojo';
async function main() {
const kojo = new Kojo({ name: 'users' });
const pool = new pg.Pool({
user: 'pg_user',
database: 'db_name',
password: 'password',
host: 'localhost'
});
kojo.set('pg', pool); // can be used as `kojo.get('pg')`
const nats = new NATS({...});
kojo.set('nats', nats);
await kojo.ready(); // await for services and subscribers to initialize
}
return main();
Services and their methods
Service is just a directory with files which represent methods. For example:
🗀 kojo/
├── 🗀 services/
│ ├── 🗀 user/
│ │ ├── 🖹 register.js
│ │ ├── 🖹 update.js
│ │ ├── 🖹 list.js
│ │ └── ...
│ └── 🗀 profile/
│ ├── 🖹 create.js
│ ├── 🖹 update.js
│ └── ...
We see two services user
and profile
both of which have some methods.
These methods are available from anywhere via kojo instance:
kojo.services.user.list()
kojo.services.profile.update()
- etc
A method file must export an async
function which (usually) returns a value.
It will have kojo instance and logger in its context:
export default async function () {
const [ kojo, logger ] = this; // instance and logger passed in context
...
const { profile } = kojo.services;
logger.debug('creating profile', userData);
return profile.create(userData);
};
Important: for method's context to be available, the method must be
defined via function() {}
, not arrow ()=>{}
Kojo is also an EventEmitter
and can publish internal events:
...
const [ kojo, logger ] = this;
...
kojo.emit('profile.created', newProfile);
...
kojo.on('profile.created', (newProfile) => {
...
});
Thus, you can create 'internal' subscribers which listen to events.
Note: Methods named test
are ignored and not registered. These are
reserved for unit tests.
Subscribers
🗀 kojo/
├── 🗀 subscribers/
│ ├── 🖹 user.register.js
│ ├── 🖹 user.update.js
│ ├── 🖹 profile.created.js
│ └── ...
Subscriber exports an async function which is called once during kojo
initialization and is not available otherwise. It is supposed to have a
single subscription to a pub/sub transport subject or services's internal
event, or http route and is recommended to be named accordingly. For
example, subscribers/internal.user.registered.js
:
export default async (kojo, logger) => {
const { user } = kojo.services;
const nats = kojo.get('nats');
user.on('registered', (newUser) => {
logger.debug('publishing notification');
nats.publish('notification', newUser);
});
};
Unlike service method, subscriber function can be defined via arrow and has kojo instance and logger as arguments, not context.
Logger
Kojo uses a custom mechanism for 'smart' logging from subscribers and services.
'Smart' means that if you log from method user.register
, log entries
will include "user.register":
logger.debug(userData);
☢ test.QOmup DEBUG [user.register] {...user data}
You can always use your own logger, provided you register it as an extra, but this logger will, of course, not have this 'smart' feature.
Docs
Read the docs.
Logic placement strategy
It is sometimes difficult to decide on where business logic should reside: subscribers or services. A rule of a thumb could be the following - place logic inside subscribers when in doubt. When code starts repeating or getting complicated - its time for introducing some services to keep it DRY and maintainable.
Subscriber should be the single point of entry for an event bound to your microservice, external or internal. You should be able to easily tell what exactly a microservice is responsible for by just looking at its subscribers directory. You can then track the rest of the logic by inspecting a subscriber and following the services it uses.
Test
npm test
Troubleshooting
If you see this error message:
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".json" for /kojo/package.json
you need to launch your service with Node's --experimental-json-modules
option:
node service.js --experimental-json-modules