README
SirexJs
Service layer architecture for Express. Sir-(vice) Ex-(press)
SirexJs was not created to be a new "framework", but more of a way of using Express to build API's.
Like the ExpressJS website says "Express is a fast, un-opinionated, minimalist web framework for Node.js. SirexJs is an opinion on how to build API's with Express.
Inspiration
SirexJs was inspired by the Microservices architecture. You can think of a SirexJs services as a: Stand-alone feature or grouping of code with its own routing table that connects to one database model. One service can easily communicate with another service.
Updates
Read more about version updates.
Postman Example
Download a example API routes for Todo app to test in Postman.
CLI
Install
Install SirexJs-CLI globally. This will help you set up you API boilerplate. The SirexJS boilerplate comes with example "Task Service" witch you can plug into a classic "To-Do" app.
npm i -g sirexjs-cli
Getting Started
Run "sirex-cli" in you project folder or where ever you want to start your new project.
The below code snippets follow the supplied "Task Service" example.
sirexjs-cli
Choose from the following options:
- init - Create new project. Navigate to your project folder or ask SirexJs to create a new project folder structure for you.
- service - Create new service. Use this option to create the folder structure and initial code to start developing your new service.
- database - Create a connection to your preferred database. Create a database initialization template. This is not needed to run SirexJs but it helps to know what to connect to.
- middleware - Create new middleware. Creates a middleware template function in "src/middleware". ExpressJs middleware function.
- thread - Create new Service Child Process. Creates a thread template function for a Service "src/services/[service_name]/threads/[thread_name]".
Example
Run development Mode
Run your application in development mode by running this command.
npm run dev
This sets up a nodemon watcher. The watcher restart your development server as soon as it detects change in "/src" folder.
For "production", you can use PM2. But this is up to you.
Start Hooks
There are three hooks you can use while your API spins up. Return a promise in these hook functions.
- beforeLoad Before anything is loaded, even environment variables
- beforeCreate After all packages where loaded and before the API spins up. You also have access to the app API object
- created API is running You also have access to the app API object
// todo-app/index.js
'use strict';
const {
Server,
Databases
} = require('sirexjs');
Server.load({
beforeLoad: async () => {
// Before anything is loaded, even environment variables
},
beforeCreate: async (app) => {
// Callback has access to nodejs instance via "app"
},
created: async (app) => {
// Callback has access to nodejs instance via "app"
}
});
Environment File
Creating a new application also creates an ".env" file. Its already setup as a development environment.
Databases
Creating a new database connection also creates a new folder which contains an example index.js file.
This new file exports a class, you can extend this class or create your own implementation.
If you want to only start the application when database is loaded, use the event hooks in Root index.js file.
You can connect any database, MongoDB, MySQL, NeDB, CouchDB or you can connect all of them. It is all up to you.
You can access your databases through:
const {
Services,
middleware,
Databases
} = require('sirexjs');
const inMemoryDBInstance = new Databases.inMemory();
Router
Adding the following route gives you access to the service sub routes.
const {
Services,
middleware
} = require('sirexjs');
router.use('/tasks', Services.tasks.routes);
Example:
// src/router/index.js
'use strict';
const express = require('express');
const router = express.Router();
const {
Services,
middleware
} = require('sirexjs');
module.exports = (function () {
router.use('/tasks', Services.tasks.routes);
router.use('*', (req, res) =>{
res.status(200).send(`Resource not be found.`);
});
return router;
})();
Services
From the Sirex-CLI Choose the options “service - Create new service” follow prompts and create your new service.
Services can be exposed through the "router" or it can be used internally by other services.
service routes
Add routes to a service
const {
Services,
middleware
} = require('sirexjs');
router.get('/', [Middleware.auth], async (req, res) => {
try {
const list = await Services.tasks.manager('index')
.init()
.list();
res.restResponse(list);
} catch (e) {
Extensions.logger.error(e);
res.restResponse(e);
}
});
Example:
// src/router/services/tasks/routes/index.js
'use strict';
const express = require('express');
const router = express.Router();
const {
Services,
Middleware,
Extensions
} = require('sirexjs');
module.exports = (function () {
router.get('/', [Middleware.auth], async (req, res) => {
try {
console.log(Services.tasks.manager);
const list = await Services.tasks.manager('index')
.init()
.list();
res.restResponse(list);
} catch (e) {
Extensions.logger.error(e);
res.restResponse(e);
}
});
router.post('/', [Middleware.auth], async (req, res) => {
try {
const created = await Services.tasks.manager('index')
.init()
.create(req.body);
res.restResponse(created);
} catch (e) {
Extensions.logger.error(e);
res.restResponse(e);
}
});
router.put('/:taskId', [Middleware.auth], async (req, res) => {
try {
const updated = await Services.tasks.manager('index')
.init()
.update(req.params.taskId, req.body);
res.restResponse(updated);
} catch (e) {
Extensions.logger.error(e);
res.restResponse(e);
}
});
router.delete('/:taskId', [Middleware.auth], async (req, res) => {
try {
const deleted = await Services.tasks.manager('index')
.init()
.delete(req.params.taskId);
res.restResponse(deleted);
} catch (e) {
Extensions.logger.error(e);
res.restResponse(e);
}
});
router.use('*', (req, res) => {
res.status(404).send(`Resource not be found.`);
});
return router;
})();
Managers
Service managers contains logic to manipulate data before you save it to a database or use the manipulated data in other parts of your application.
You can access the manager through "Services".
Example:
const {
Services,
Middleware,
Extensions
} = require('sirexjs');
const list = await Services.tasks.manager('index')
.init()
.list();
Example - Task Manager
// src/router/services/tasks/managers/index.js
'use strict';
const {
Services,
Middleware,
Extensions
} = require('sirexjs');
module.exports = class tasksManager {
static init() {
return new this();
}
list() {
return [];
}
create(data) {
return {};
}
update(taskId, data) {
return {};
}
delete(taskId) {
return {};
}
};
Models
When you create a service a model folder structure is created by default. You can extend the model class with the database connection you created before.
With that said, its all up to you how you structure your models.
Extensions
These methods are there to make your development process easier.
Logging
Logger is and extension of Winston. For more about how to use logger go here.
Examples:
const {
Services,
Middleware,
Extensions
} = require('sirexjs');
Extensions.logger.info("Info logs here");
Extensions.logger.error("Error logs here");
Validation
Validation uses validator internally. It was modified to be a bit more "compact". Validation also has the ability to validate nested properties.
Flat validation
const {
Services,
Middleware,
Extensions
} = require('sirexjs');
const validate = Extensions.validation();
validate.setValidFields({
'email': {
'rules': 'required|email',
'field_name': 'Local email'
},
'address': {
'props': 'required|email',
'field_name': 'Local email'
}
});
if (validate.isValid(data)) {
Extensions.logger.info(validate.fields);
} else {
throw Extensions.exceptions(404, 'Could not create new user', validate.errors);
}
Nested validation
const {
Services,
Middleware,
Extensions
} = require('sirexjs');
const validate = Extensions.validation();
validate.setValidFields({
'firstName': {
'rules': 'required',
field_name: 'Your name'
},
'address': {
'props': {
'prop_1': {
'rules': 'required',
field_name: 'State'
},
'prop_2': {
'rules': 'required|number',
field_name: 'Postcode'
},
}
}
});
if (validate.isValid(data)) {
Extensions.logger.info(validate.fields);
} else {
throw Extensions.exceptions(404, 'Could not create new user', validate.errors);
}
Types:
- string
- integer
- float
- boolean
- date
Require a field and type:
{
'userName': {
'rules': 'string'
},
'email': {
'rules': 'required|email'
}
}
Threads
Node is single threaded and because of this any long running processes will block the event loop. This creates latency in your application. Using threads you can offload any long running process to a separate Child Process.
Features:
- New threads only spin up when requested.
- Previously created threads are re-used as spinning up a thread takes about 2 seconds.
- Idle threads waits for 5 seconds if not reused in that time it shuts down.
- Max 5 threads can be running at the same time.
- Request are placed in a "thread pool" if more than 5 threads are active.
Using it as a extension:
// Tread function for "tasks service"
// Threads have to return a function
'use strict';
const {
Services
} = require('sirexjs');
// createFile Thread function
module.exports = function() {
// Put some long running code here.
return 'something'; // Return something when you are done
};
// Run "tasks service" tread, or thread from another location
'use strict';
const {
Services,
Middleware,
Extensions
} = require('sirexjs');
// Run thread attache to service
let thread = await Services.tasks.thread('createFile', ['arg1','arg2','arg3'])
.received();
// Run thread function from any location
let thread = await Extensions.threads('/services/tasks/managers/treadFunction', ['hello'])
.received();
Exceptions
API response and exceptions functions work together. Make sure all exceptions caught is eventually passed to the route response of the API end-points to display any error messages to the user. There are 3 kinds of exceptions that can be thrown.
Exception types:
- Response
- System
- Standard
All exceptions except "Response" will trigger a stack trace error log.
Response
Used when you have validation error to handle.
Example:
sirexjs.Extensions.exceptions.response(http_response_code, 'Description', collection_of_errors);
const {
Services,
Middleware,
Extensions
} = require('sirexjs');
throw Extensions.exceptions.response(400, 'Could not create new user', validate.errors);
// Endpoint Response:
{
"message": "Following fields are invalid.",
"endpoint": "/user/sign-up",
"method": "POST",
"timestamp": "2019-10-24T14:03:22.780Z",
"errors": {
"email": "Email is not a valid email format"
}
}
System
Error relating to the API application.
Example:
sirexjs.Extensions.exceptions.system('your_message_here');
const {
Services,
Middleware,
Extensions
} = require('sirexjs');
throw Extensions.exceptions.system('Internal conflict found.');
// Endpoint Response:
{
"message": "Internal conflict found.",
"endpoint": "/user/sign-up",
"method": "POST",
"timestamp": "2019-10-24T13:49:22.388Z"
}
Standard
Any exceptions thrown by the application. The the API response example is below. The stack trace error will be logged.
// Stack trace reference
"Trace: refId: 3416e49bf1f4758234345cb79d1550ff Timestamp: 2020-08-28T12:42:02Z"
// Endpoint Response:
{
"message": "We seem to have a problem. Please contact support and reference the included information.",
"endpoint": "/tasks",
"method": "GET",
"timestamp": "2020-08-28T12:42:02Z",
"refId": "3416e49bf1f4758234345cb79d1550ff"
}
API Response
Display data back to users.
The response function can handle succeeded and exception responses to the user.
res.restResponse(responseData);
Example:
router.post('/sign-up', async (req, res) => {
try {
let user = {
firstName: 'Name'
};
res.restResponse(user);
} catch(e) {
res.restResponse(e);
}
});