An event-driven microservice framework. KΕjΕ (ε·₯ε ΄) means 'factory' in Japanese.
Kojo is straightforward: it has subscribers (event handlers, routes, or endpoints) and functions (reusable business logic). Subscribers subscribe to pub/sub, request/response, or scheduled events from your chosen transport, and functions perform the business logic.
Note: If you're upgrading from v8.x, see the migration guide. TL;DR:
services/βfunctions/,serviceDirβfunctionsDir
npm i kojo
- π― Root-level functions: No need to create directories for simple functions (
functions/generateId.js) - π§ Flexible naming: Use
functions/,ops/, or any name that fits your domain β οΈ Breaking change: Default directory renamedservices/βfunctions/- Full migration guide β
NOTE: This package uses native ESM modules (since v8.0.0).
Create a function group with methods (functions/user/create.js):
export default async function (userData) {
const [ kojo, logger ] = this; // kojo instance and logger
const { pg: pool } = kojo.state; // get previously set pg connection
logger.debug('creating', userData); // logger automatically adds function and method name
const query = `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *`;
const result = await pool.query(query, [userData.name, userData.email]);
const newRecord = result ? result.rows[0] : null;
if (newRecord)
logger.info('created', newRecord);
return newRecord;
}Access: kojo.functions.user.create({ name: 'Alice', email: 'alice@example.com' })
For simple utilities, place them directly in the functions directory (functions/generateId.js):
export default async function () {
const [ kojo, logger ] = this;
logger.debug('generating unique ID');
return crypto.randomUUID();
}Access: kojo.functions.generateId()
Create a subscriber (subscribers/user.create.js):
export default async (kojo, logger) => {
const { user } = kojo.functions; // we defined `user` function group above
const { nats } = kojo.state; // get nats connection from state
nats.subscribe('user.create', async (userData) => {
logger.debug('received user.create event', userData);
const newUser = await user.create(userData);
if (newUser) {
logger.info('user created, publishing event');
nats.publish('user.created', newUser);
}
});
}Initialize Kojo and add connections:
import Kojo from 'kojo';
import pg from 'pg';
import NATS from 'nats';
async function main() {
const kojo = new Kojo({
name: 'users',
icon: 'π₯'
});
// Set up connections
const pool = new pg.Pool({
user: 'pg_user',
database: 'db_name',
password: 'password',
host: 'localhost'
});
kojo.set('pg', pool); // accessible via kojo.get('pg')
const nats = await NATS.connect({ servers: 'nats://localhost:4222' });
kojo.set('nats', nats);
// Initialize - loads all functions and subscribers
await kojo.ready();
console.log('Kojo ready! π');
}
main().catch(console.error);Kojo supports two ways to organize functions:
A function group is a directory with files representing methods:
π my-app/
βββ π functions/
β βββ π user/ β Function group
β β βββ πΉ register.js
β β βββ πΉ update.js
β β βββ πΉ list.js
β β βββ πΉ test.js β Ignored (reserved for unit tests)
β βββ π profile/ β Another function group
β β βββ πΉ create.js
β β βββ πΉ update.js
β βββ πΉ generateId.js β Root-level function (NEW in v9!)
These are available via:
kojo.functions.user.list()kojo.functions.profile.update()kojo.functions.generateId()
For simple utilities, place files directly in functions/:
// functions/hashPassword.js
export default async function (password) {
const [ kojo, logger ] = this;
logger.debug('hashing password');
return bcrypt.hash(password, 10);
}Access: kojo.functions.hashPassword('secret123')
All functions receive kojo instance and logger via context:
export default async function (userData) {
const [ kojo, logger ] = this; // context injection
const { profile } = kojo.functions; // access other functions
logger.debug('creating profile', userData);
return profile.create(userData);
}function() {} syntax, NOT arrow functions () => {}, to receive context.
Kojo extends EventEmitter, allowing internal pub/sub:
// In a function - emit an event
export default async function (userData) {
const [ kojo, logger ] = this;
const newProfile = await createProfile(userData);
kojo.emit('profile.created', newProfile);
return newProfile;
}// In a subscriber - listen to internal events
export default async (kojo, logger) => {
kojo.on('profile.created', (newProfile) => {
logger.info('Profile created internally', newProfile.id);
// Send notification, update cache, etc.
});
};Note: Files named test.js are automatically ignored (reserved for unit tests).
π my-app/
βββ π subscribers/
β βββ πΉ user.register.js β External event handler
β βββ πΉ user.update.js β External event handler
β βββ πΉ internal.user.created.js β Internal event handler
β βββ πΉ http.get.users.js β HTTP route handler
A subscriber exports an async function called once during initialization. It sets up event listeners, HTTP routes, or scheduled tasks. Name files to reflect what they handle.
Example - Internal event subscriber (subscribers/internal.user.registered.js):
export default async (kojo, logger) => {
const { user } = kojo.functions;
const nats = kojo.get('nats');
kojo.on('user.registered', (newUser) => {
logger.info('user registered, sending notification', newUser.id);
nats.publish('notification.send', {
userId: newUser.id,
type: 'welcome'
});
});
}Example - HTTP route subscriber (subscribers/http.get.users.js):
export default async (kojo, logger) => {
const { user } = kojo.functions;
const app = kojo.get('express');
app.get('/users', async (req, res) => {
logger.debug('GET /users');
const users = await user.list();
res.json(users);
});
}Note: Unlike functions, subscribers can use arrow functions and receive kojo/logger as arguments, not context.
Kojo provides automatic context-aware logging. When logging from user.register, entries automatically include the function and method name:
// In functions/user/register.js
logger.debug('registering user', userData);Output:
π₯ users.Xk9pL DEBUG [user.register] registering user {...user data}
The logger automatically adds:
- Instance name and ID (
users.Xk9pL) - Function and method name (
[user.register]) - Support for additional context via
logger.setCustomTag('request-id-123')
You can use your own logger by setting it as state (kojo.set('logger', customLogger)), but you'll lose the automatic context tagging.
Read the [docs].
new Kojo({
subsDir: 'subscribers', // Subscribers directory (default)
functionsDir: 'functions', // Functions directory (default)
name: 'ε·₯ε ΄', // Instance name (default: factory)
icon: 'β’', // Display icon (default)
logLevel: 'debug', // Log level: debug, info, warn, error, silent
loggerIdSuffix: false, // Append instance ID to logs (default: false)
parentPackage: null // Parent package.json for version display
})The directory name determines the API property name:
// Default
new Kojo({ functionsDir: 'functions' })
β kojo.functions.*
// Domain-specific naming
new Kojo({ functionsDir: 'ops' })
β kojo.ops.*
new Kojo({ functionsDir: 'handlers' })
β kojo.handlers.*Rule of thumb: Place logic in subscribers when in doubt. Move to functions when:
- Code starts repeating across subscribers
- Logic becomes complex and needs to be DRY
- Functionality needs to be reusable
Subscribers are entry points - they make it obvious what events your microservice handles. Functions contain the reusable business logic. By examining the subscribers directory, you should immediately understand what the microservice does.
npm test
If you see:
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".json"
Launch your service with:
node service.js --experimental-json-modulesOr update to Node.js 18+ which handles JSON imports better.
If you see:
Warning: "serviceDir" is deprecated. Please use "functionsDir" instead.
Update your config:
// Old
new Kojo({ serviceDir: 'services' })
// New
new Kojo({ functionsDir: 'services' })