Chyz is an enterprise-grade, modular, decorator-driven Node.js & TypeScript microservice framework inspired by the battle-tested architecture of the Yii2 PHP Framework. It combines the agility of Express with strong object-oriented principles, declarative routing decorators, Active-Record-style Sequelize models, hierarchical Role-Based Access Control (RBAC), and pluggable authentication behaviors.
- Key Features
- Architecture Overview
- Installation
- TypeScript Configuration
- Recommended Directory Structure
- Quick Start
- Core Concepts & Detailed Guide
- 1. Application Configuration & Bootstrapping
- 2. Decorator-Driven Controllers
- 3. Multiple Controller Prefixes (Multi-Versioning)
- 4. Behaviors & Action Filters
- 5. Active Record Models & ModelManager
- 6. Model Validation & Safe Attribute Loading
- 7. Database Transactions
- 8. Identity Management & Authentication
- 9. Role-Based Access Control (RBAC)
- 10. Keycloak SSO / OIDC Integration
- 11. Lifecycle Events (CEvents)
- 12. Enterprise Logging (log4js)
- 13. Exception Handling Hierarchy
- API Reference
- Development & Scripts
- License & Authors
- Component-Based Architecture: Modular configuration matching Yii2's service locator pattern (
BaseChyz.getComponent(...)). - Declarative Decorator Routing: Intuitive HTTP routing with
@controller,@get,@post,@put,@delete, and@Middleware. - Multiple Prefix Support: Seamlessly bind a single controller to multiple API prefixes/versions simultaneously (e.g.,
/api/subaccountand/api/v4/subaccount). - Action Filters & Behaviors: Clean request interception with
beforeAction,afterAction,AccessControl, and authenticators. - Built-in Authentication Strategies:
JwtHttpBearerAuth: Token-based JWT Bearer authentication.KeyCloakHttpBearerAuth: Enterprise OpenID Connect / Keycloak integration.HttpBearerAuth: Generic bearer token authentication.HttpBasicAuth: Standard HTTP Basic authentication.HttpHeaderAuth: API key / custom header authentication.
- Active Record ORM Layer: Powerful
Modelbase class wrapping Sequelize, supporting schema definitions, relations (hasOne,hasMany,belongsTo,belongsToMany), hooks, and validation. - Auto-Discovery via
ModelManager: Automatically scans and registers models into a global, type-accessible registry. - Hierarchical RBAC Engine: Database-backed Role-Based Access Control with roles, permissions, child relationships, and dynamic business rule checks.
- Safe Data Loading (
load()): Mass-assignment protection and form scoping inspired by Yii2. - Unified Event Dispatcher (
CEvents): Subscribe to framework lifecycle stages (ON_INIT_BEFORE,ON_BEFORE_ACTION,ON_START, etc.). - Enterprise Diagnostics: Multi-appender logging powered by
log4js. - Production-Ready Middlewares: Integrated Gzip compression, CORS, Body-Parser (JSON & URL-encoded), Method-Override, and SSL/HTTPS support.
┌──────────────────────────────────────┐
│ Incoming HTTP │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ Express Engine & Global Middlewares│
│ (CORS, Compression, BodyParser) │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ BaseChyz Controller Dispatcher │
│ (Supports Multiple Route Prefixes) │
└──────────────────┬───────────────────┘
▼
┌────────────────────────────────────────────────────┐
│ Controller Lifecycle │
│ │
│ 1. beforeAction(route, req, res) │
│ ├── Authenticator Filter (JWT / Keycloak) │
│ ├── AccessControl Filter (RBAC Rules) │
│ └── Custom ActionFilter Behaviors │
│ │
│ 2. Action Handler Execution (@get, @post, ...) │
│ ├── ModelManager & Active Record Models │
│ ├── Data Loading (model.load(req.body)) │
│ ├── Database Transactions │
│ └── Business Logic Execution │
│ │
│ 3. afterAction(route, req, res) │
└──────────────────┬─────────────────────────────────┘
▼
┌──────────────────────────────────────┐
│ Standardized JSON / XML │
│ or Unified Error Responses │
└──────────────────────────────────────┘
Install Chyz and its required peer dependencies:
npm install chyz reflect-metadata sequelize pg pg-hstore jsonwebtoken
# or using yarn:
yarn add chyz reflect-metadata sequelize pg pg-hstore jsonwebtokenNote: For MySQL, MariaDB, or SQLite, replace
pg pg-hstorewithmysql2,mariadb, orsqlite3.
Because Chyz utilizes TypeScript decorators and metadata reflection, ensure experimentalDecorators and emitDecoratorMetadata are enabled in your tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["ES2022"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*", "Controllers/**/*", "Models/**/*"]
}my-chyz-service/
├── Controllers/
│ ├── ApiController.ts
│ └── UserController.ts
├── Models/
│ ├── index.ts
│ ├── User.ts
│ ├── Product.ts
│ └── Order.ts
├── config/
│ └── log4js.json
├── .env
├── index.ts
├── package.json
└── tsconfig.json
Important: Model filenames should match
<Name>.tsand export a class named<Name>Class(or inherit fromModel).ModelManagerautomatically scansControllers/../Modelsand attaches them asModelManager.<Name>.
Create your main entry point (index.ts):
import 'reflect-metadata';
import dotenvFlow from 'dotenv-flow';
dotenvFlow.config();
import Chyz, { BaseChyz, DbConnection, WebUser, Logs } from 'chyz';
import { AuthManager } from 'chyz/rbac/AuthManager';
import { User } from './Models/User';
const config = {
port: process.env.PORT || 3001,
controllerpath: __dirname + '/Controllers',
staticFilePath: __dirname + '/public',
components: {
// Database connection component
db: {
class: DbConnection,
database: process.env.DB_NAME || 'my_database',
username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || '',
options: {
host: process.env.DB_HOST || '127.0.0.1',
port: parseInt(process.env.DB_PORT || '5432'),
dialect: 'postgres', // 'mysql' | 'mariadb' | 'postgres' | 'mssql'
logging: (msg: string) => BaseChyz.debug('[SQL]', msg)
}
},
// RBAC Authorization Manager
authManager: {
class: AuthManager
},
// User Identity Component
user: {
class: WebUser,
identityClass: User
}
}
};
// Bootstrap application and listen
Chyz.app(config).then((server) => server.Start());BaseChyz.app(config) initializes all configured components in sequence:
dbcomponent is initialized first to establish database availability.- Other application components (
user,authManager, custom components) are instantiated and theirinit()hooks are executed. - Express middlewares (JSON body parser, URL-encoded parser, Method-Override, CORS, Compression) are loaded.
- Auto-discovery loads all models in the
Modelsdirectory and registers them intoModelManager. - Controllers from
controllerpathare imported, inspected for metadata decorators, and registered with Express.
You can access any registered component globally across your application via:
const db = BaseChyz.getComponent('db');
const userComponent = BaseChyz.getComponent('user');
const authManager = BaseChyz.getComponent('authManager');Controllers extend CWebController and define route endpoints using method decorators:
import {
CWebController,
controller,
get,
post,
put,
delete as del,
Request,
Response
} from 'chyz';
@controller('/api/v1/products')
export class ProductController extends CWebController {
@get('/')
public async list(req: Request, res: Response) {
return res.json({ success: true, data: [] });
}
@get('/:id')
public async view(req: Request, res: Response) {
const { id } = req.params;
return res.json({ id });
}
@post('/')
public async create(req: Request, res: Response) {
return res.status(201).json({ created: req.body });
}
@put('/:id')
public async update(req: Request, res: Response) {
return res.json({ updated: req.params.id, data: req.body });
}
@del('/:id')
public async remove(req: Request, res: Response) {
return res.json({ deleted: req.params.id });
}
}
export default ProductController;Chyz supports multiple @controller decorators on the same class. This allows you to serve endpoints across different API versions or paths without code duplication:
import { CWebController, controller, get, Request, Response } from 'chyz';
@controller('/api/subaccount')
@controller('/api/v4/subaccount')
export class SubAccountController extends CWebController {
@get('/profile')
public getProfile(req: Request, res: Response) {
// Accessible via:
// GET /api/subaccount/profile
// GET /api/v4/subaccount/profile
return res.json({ message: 'Subaccount profile' });
}
}Just like Yii2's behaviors(), any controller can declare cross-cutting filters such as authentication and RBAC access rules:
import {
CWebController,
controller,
get,
post,
Request,
Response,
JwtHttpBearerAuth,
AccessControl
} from 'chyz';
@controller('/api/orders')
export class OrderController extends CWebController {
public behaviors(): any[] {
return [
{
// Authentication filter
authenticator: {
class: JwtHttpBearerAuth,
except: ['public-feed'] // Exclude specific actions
},
// Role & Permission access control
access: {
class: AccessControl,
rules: [
{
allow: true,
actions: ['public-feed'],
roles: ['?'] // '?' matches guests
},
{
allow: true,
actions: ['create', 'list'],
roles: ['@'] // '@' matches authenticated users
},
{
allow: true,
actions: ['delete'],
roles: ['admin'] // requires 'admin' RBAC role
}
]
}
}
];
}
@get('public-feed')
public publicFeed(req: Request, res: Response) {
return res.json({ feed: [] });
}
@post('create')
public create(req: Request, res: Response) {
return res.json({ status: 'created' });
}
}Chyz models inherit from Model and configure Sequelize attributes and relational associations:
// Models/Product.ts
import { Model, DataTypes, Relation } from 'chyz/base/Model';
import { ModelManager } from 'chyz';
export class ProductClass extends Model {
public tableName(): string {
return 'products';
}
public attributes() {
return {
title: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [3, 255]
}
},
price: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false
},
category_id: {
type: DataTypes.INTEGER,
allowNull: true
}
};
}
public relations(): Relation[] {
return [
{
type: 'belongsTo',
model: ModelManager.Category.model(),
foreignKey: 'category_id',
as: 'category'
}
];
}
}Models placed inside the Models directory are automatically discovered and registered:
import { ModelManager } from 'chyz';
// Find all products
const products = await ModelManager.Product.findAll({
include: [{ model: ModelManager.Category.model(), as: 'category' }]
});
// Find one by primary key or criteria
const product = await ModelManager.Product.findOne({
where: { id: 10 }
});Chyz implements Yii2-style safe loading using load(data, formName):
import { ModelManager, ValidationHttpException } from 'chyz';
// If req.body is { Product: { title: "Laptop", price: 1200 } }
const product = ModelManager.Product;
if (product.load(req.body, 'Product')) {
const result = await product.save();
if (!result) {
// Model errors are automatically gathered
throw new ValidationHttpException(product.errors);
}
}Transactions are simple to manage via BaseChyz.getComponent("db").transaction():
import { BaseChyz, ModelManager, ValidationHttpException } from 'chyz';
let transaction: any;
try {
const db = BaseChyz.getComponent('db');
transaction = await db.transaction();
const customer = ModelManager.Customer;
customer.load(req.body, 'Customer');
const cus = await customer.save({}, { transaction });
if (!cus) {
throw new ValidationHttpException(customer.errors);
}
const order = ModelManager.Order;
order.load(req.body, 'Order');
order.setAttribute({ customer_id: cus.id });
const ord = await order.save({}, { transaction });
if (!ord) {
throw new ValidationHttpException(order.errors);
}
await transaction.commit();
} catch (error) {
if (transaction) {
await transaction.rollback();
}
throw error;
}Define your User model by implementing IdentityInterface:
// Models/User.ts
import { Model, DataTypes } from 'chyz/base/Model';
import { IdentityInterface } from 'chyz/web/IdentityInterface';
import { BaseChyz } from 'chyz';
import jwt from 'jsonwebtoken';
export class UserClass extends Model implements IdentityInterface {
public tableName(): string {
return 'users';
}
public attributes() {
return {
username: { type: DataTypes.STRING, allowNull: false },
password: { type: DataTypes.STRING, allowNull: false },
authkey: { type: DataTypes.STRING, allowNull: true },
status: { type: DataTypes.STRING, defaultValue: 'active' }
};
}
public async findIdentityByAccessToken(token: string, type: string) {
const decoded: any = jwt.decode(token, { complete: true });
if (!decoded?.payload?.user) {
return null;
}
const identity = await this.findOne({ where: { id: decoded.payload.user } });
if (identity) {
try {
jwt.verify(token, identity.authkey);
this.setIdentity(identity);
return this;
} catch (err) {
BaseChyz.debug('Token verification failed:', err);
return null;
}
}
return null;
}
public getId(): number {
return this.model().id;
}
public getAuthKey(): string {
return this.model().authkey;
}
public validateAuthKey(authKey: string): boolean {
return this.getAuthKey() === authKey;
}
}Chyz features a full-featured hierarchical RBAC system based on Yii2's auth schema (auth_item, auth_item_child, auth_assignment):
- AuthItem: Defines roles (type:
1) and permissions (type:2). - AuthItemChild: Establishes parent-child relationships (e.g.,
admininherits fromeditor, which haspostCreate). - AuthAssignment: Assigns users to roles.
const authManager = BaseChyz.getComponent('authManager');
// Check if user has permission
const hasPermission = await authManager.checkAccess(userId, 'order/delete');
// Retrieve all roles for a user
const roles = await authManager.getRolesByUser(userId);
// Retrieve all permissions for a user
const permissions = await authManager.getPermissionsByUser(userId);Chyz provides native Keycloak integration for centralized enterprise SSO:
import Keycloak from 'keycloak-connect';
import session from 'express-session';
import Chyz, { WebUser, KeyCloakHttpBearerAuth } from 'chyz';
const keycloak = new Keycloak({ scope: 'offline_access' }, {
realm: 'EnterpriseRealm',
'auth-server-url': 'https://sso.example.com/auth/',
resource: 'my-microservice',
bearerOnly: true
});
const config = {
port: 3000,
controllerpath: __dirname + '/Controllers',
components: {
user: {
class: WebUser,
identityClass: KeycloakUser
}
},
middlewares: {
keycloak: {
keycloak: keycloak,
config: { logout: '/logout' }
}
}
};
Chyz.app(config).then((server) => server.Start());In your controller, use KeyCloakHttpBearerAuth:
public behaviors(): any[] {
return [{
authenticator: {
class: KeyCloakHttpBearerAuth
}
}];
}Hook into any lifecycle event using BaseChyz.EventEmitter:
import { BaseChyz, CEvents } from 'chyz';
BaseChyz.EventEmitter.on(CEvents.ON_INIT_BEFORE, (app, config) => {
BaseChyz.info('Bootstrapping initialized...');
});
BaseChyz.EventEmitter.on(CEvents.ON_BEFORE_ACTION, (controller, req, res) => {
BaseChyz.debug(`Action invoked: ${controller.id}/${req.path}`);
});
BaseChyz.EventEmitter.on(CEvents.ON_START, () => {
BaseChyz.info('Server successfully listening.');
});Available CEvents:
ON_INIT_BEFORE: Triggered before configuration initialization.ON_INIT_AFTER: Triggered after components and controllers are loaded.ON_MIDDLEWARE: Triggered when Express middleware chain is bound.ON_BEFORE_START: Triggered right beforelisten()starts.ON_START: Triggered when HTTP/HTTPS server is up and listening.ON_BEFORE_ACTION: Triggered before controller action execution.ON_AFTER_ACTION: Triggered after controller action execution.ON_DB_CONNECTION: Triggered on successful DB connection.ON_DB_DISCONNECT: Triggered on DB disconnect.
Chyz comes equipped with structured logging via log4js:
import { BaseChyz, Logs } from 'chyz';
// Configure custom log4js configuration
const customLogs = new Logs('MyService', require('./config/log4js.json'));
const config = {
logs: customLogs,
// ...
};
// Logging methods
BaseChyz.debug('Debug payload', { userId: 42 });
BaseChyz.info('Service processed request');
BaseChyz.warn('Cache miss encountered');
BaseChyz.error('Unhandled exception', error);
BaseChyz.fatal('Critical system shutdown');Throw semantic HTTP exceptions in your services or controllers. Chyz automatically formats them into consistent JSON responses:
import {
BadRequestHttpException,
UnauthorizedHttpException,
ForbiddenHttpException,
NotFoundHttpException,
ValidationHttpException
} from 'chyz';
// 400 Bad Request
throw new BadRequestHttpException('Invalid request parameters');
// 401 Unauthorized
throw new UnauthorizedHttpException('Session expired');
// 403 Forbidden
throw new ForbiddenHttpException('Insufficient permissions');
// 404 Not Found
throw new NotFoundHttpException('Customer not found');
// 422 / Validation Error
throw new ValidationHttpException({ email: ['Email format is invalid'] });| Decorator | Target | Description |
|---|---|---|
@controller(prefix: string) |
Class | Registers controller base path. Supports stacking multiple decorators for multiple prefixes. |
@get(path: string) |
Method | Binds HTTP GET route to the controller method. |
@post(path: string) |
Method | Binds HTTP POST route to the controller method. |
@put(path: string) |
Method | Binds HTTP PUT route to the controller method. |
@delete(path: string) |
Method | Binds HTTP DELETE route to the controller method. |
@Middleware(handlers[]) |
Method | Attaches Express RequestHandler middleware directly to the route. |
| Class | Purpose |
|---|---|
BaseChyz |
Main application instance, component registry, logging facade, and lifecycle manager. |
CWebController |
Base class for web controllers offering beforeAction, afterAction, and behaviors(). |
Model |
Active Record abstraction wrapping Sequelize models, relations, transactions, and data loading. |
ModelManager |
Global registry containing auto-discovered Active Record models. |
Component |
Base object for lifecycle-managed components (init() hook). |
DbConnection |
Sequelize database manager component supporting connection pooling and transactions. |
WebUser |
Identity context representing the currently authenticated user in request scope. |
AuthManager |
RBAC service providing role/permission hierarchy evaluation. |
AccessControl |
Action filter behavior enforcing RBAC and role match rules per route. |
JwtHttpBearerAuth |
Bearer token authentication filter decoding and validating JWT tokens. |
To run or build the framework locally:
# Debug / run with ts-node
yarn debug
# Build TypeScript to dist/ with assets and package configs
yarn build
# Run build on Windows environment
yarn build-win- Author: Cihan Öztürk (cihan@chy.com.tr)
- Organization: Chy Bilgisayar Bilişim
- License: MIT