@expressive-tea/commons
The foundation that powers decorator magic in Expressive Tea
Versioning: This package uses Calendar Versioning (CalVer) in the format
YYYY.MM.MICRO(e.g.,2026.1.0). Learn more โ
Version Compatibility
This package (v2026.1.0) requires
@expressive-tea/core>= 2.0.0
- Compatible:
@expressive-tea/core@2.x.xand above- Not compatible:
@expressive-tea/core@1.x.xor belowUsing Expressive Tea Core v1.x? You'll need
@expressive-tea/commons@1.0.1(last SemVer version for Core v1.x).Expressive Tea Core Reference: This package is designed to work with Expressive Tea Core - the main framework.
Metadata Package
As of v2026.1.0, all metadata functionality has been extracted to @expressive-tea/metadata
This package now re-exports all metadata APIs from
@expressive-tea/metadatafor backward compatibility:
Metadataclass (metadata management)@SetMetadata,@Meta,@InheritMetadata,@CacheInMetadata,@DeprecateddecoratorsgetClasshelper utilityFor new projects, consider using
@expressive-tea/metadatadirectly for better tree-shaking and smaller bundle sizes.For existing projects, no changes needed - everything works the same way!
Why Commons?
Ever wondered how decorators like @Route or @Inject actually work under the hood? That's where commons comes in! This package is the secret sauce that makes TypeScript decorators feel like pure magic
Think of it as your metadata Swiss Army knife - a lean, mean, type-safe wrapper around reflect-metadata that powers the entire Expressive Tea ecosystem.
Features That'll Make You Smile
- Metadata Management - Store and retrieve data on classes, methods, and properties with ease
- Decorator Utilities - Ready-to-use decorators (@SetMetadata, @Meta, @Deprecated, and more!)
- TypeScript-First - Written in TS5 with strict mode - your IDE will love you
- 100% Type-Safe - No more
anynightmares in your decorator code - Feather-Light - Zero dependencies except
reflect-metadata - Battle-Tested - 94%+ test coverage - we don't ship bugs
- Self-Documenting - Full JSDoc on every method - no docs, no merge!
Installation
# npm
npm install @expressive-tea/commons reflect-metadata
# yarn
yarn add @expressive-tea/commons reflect-metadata
# pnpm
pnpm add @expressive-tea/commons reflect-metadata
Requirements
- Node.js โฅ 18.0.0 (LTS recommended)
- TypeScript โฅ 5.0.0 (if you're using TypeScript)
- reflect-metadata 0.2.x
TypeScript Setup
Add this to your tsconfig.json (don't skip this!):
{
"compilerOptions": {
"experimentalDecorators": true, // ๐จ Enable decorator magic
"emitDecoratorMetadata": true, // ๐ Emit design-time type info
"target": "ES2017",
"module": "commonjs"
}
}
Usage Examples
Creating Your First Decorator
Let's build a @Route decorator from scratch:
import 'reflect-metadata';
import { Metadata } from '@expressive-tea/commons';
// 1. Create the decorator
function Route(path: string) {
return function (target: any, propertyKey: string) {
Metadata.set('route:path', path, target, propertyKey);
};
}
// 2. Use it on a class
class UserController {
@Route('/users')
getUsers() {
return [];
}
@Route('/users/:id')
getUser() {
return {};
}
}
// 3. Read the metadata
const routes = Metadata.get('route:path', UserController.prototype, 'getUsers');
console.log(routes); // '/users'
BOOM! You just created a decorator system!
Class-Level Metadata
Decorators aren't just for methods - classes can have metadata too:
import { Metadata } from '@expressive-tea/commons';
// Mark a class as a controller
function Controller(basePath: string) {
return function (target: Function) {
Metadata.set('controller:basePath', basePath, target);
Metadata.set('controller:isController', true, target);
};
}
@Controller('/api/v1')
class ApiController {}
// Retrieve it later
const basePath = Metadata.get('controller:basePath', ApiController);
console.log(basePath); // '/api/v1'
// Check if it's a controller
if (Metadata.has('controller:isController', ApiController)) {
console.log('Yes, this is a controller! ๐ฎ');
}
// Get all metadata keys
const keys = Metadata.getKeys(ApiController);
console.log(keys); // ['controller:basePath', 'controller:isController']
Property Decorators
Need to inject dependencies? Property metadata has your back:
import { Metadata } from '@expressive-tea/commons';
// Dependency injection decorator
function Inject(token: string) {
return function (target: any, propertyKey: string) {
Metadata.set('inject:token', token, target, propertyKey);
};
}
class UserService {
@Inject('DATABASE')
private db: Database;
@Inject('LOGGER')
private logger: Logger;
}
// Build your DI container
function getInjectableProperties(target: any) {
return Object.getOwnPropertyNames(target)
.filter(prop => Metadata.has('inject:token', target, prop))
.map(prop => ({
property: prop,
token: Metadata.get('inject:token', target, prop)
}));
}
const injectables = getInjectableProperties(UserService.prototype);
console.log(injectables);
// [
// { property: 'db', token: 'DATABASE' },
// { property: 'logger', token: 'LOGGER' }
// ]
Parameter Decorators
Level up with parameter metadata:
import { Metadata } from '@expressive-tea/commons';
// Body parameter decorator
function Body() {
return function (target: any, propertyKey: string, parameterIndex: number) {
const existingParams = Metadata.get('params:body', target, propertyKey) || [];
existingParams.push(parameterIndex);
Metadata.set('params:body', existingParams, target, propertyKey);
};
}
// Query parameter decorator
function Query(name: string) {
return function (target: any, propertyKey: string, parameterIndex: number) {
const existingParams = Metadata.get('params:query', target, propertyKey) || {};
existingParams[parameterIndex] = name;
Metadata.set('params:query', existingParams, target, propertyKey);
};
}
class AuthController {
login(
@Body() credentials: LoginDto,
@Query('rememberMe') remember: boolean
) {
// Your login logic
}
}
// Extract parameter metadata
const bodyParams = Metadata.get('params:body', AuthController.prototype, 'login');
const queryParams = Metadata.get('params:query', AuthController.prototype, 'login');
console.log('Body parameters at indices:', bodyParams); // [0]
console.log('Query parameters:', queryParams); // { 1: 'rememberMe' }
Design-Time Type Information
TypeScript can tell you the types at design time:
import { Metadata } from '@expressive-tea/commons';
class BlogService {
createPost(title: string, content: string): Promise<Post> {
return Promise.resolve({} as Post);
}
}
// Get parameter types
const paramTypes = Metadata.getParamTypes(BlogService.prototype, 'createPost');
console.log(paramTypes); // [String, String]
// Get return type
const returnType = Metadata.getReturnType(BlogService.prototype, 'createPost');
console.log(returnType); // Promise
// Get design type
const designType = Metadata.getType(BlogService.prototype, 'createPost');
console.log(designType); // Function
Helper Utilities
import { getClass } from '@expressive-tea/commons';
const instance = new UserController();
const Constructor = getClass(instance);
console.log(Constructor.name); // 'UserController'
console.log(Constructor === UserController); // true
Decorator Utilities
NEW in v2.0.0! Commons now includes ready-to-use decorator utilities to make working with metadata even easier.
@SetMetadata
Set metadata with a single decorator:
import { SetMetadata, Metadata } from '@expressive-tea/commons';
// Static value
@SetMetadata('role', 'admin')
class AdminController {}
// Dynamic value with factory function
@SetMetadata('timestamp', () => Date.now())
class MyClass {}
// On methods
class UserController {
@SetMetadata('route:path', '/users')
@SetMetadata('route:method', 'GET')
getUsers() {}
}
// Retrieve the metadata
const role = Metadata.get('role', AdminController); // 'admin'
const path = Metadata.get('route:path', UserController.prototype, 'getUsers'); // '/users'
@Meta
Set multiple metadata key-value pairs at once:
import { Meta, Metadata } from '@expressive-tea/commons';
// Multiple values on a class
@Meta({
controller: true,
basePath: '/api',
version: 'v1',
deprecated: false
})
class ApiController {}
// Multiple values on a method
class ProductController {
@Meta({
'route:path': '/products',
'route:method': 'GET',
'cache:ttl': 3600,
'auth:required': true
})
getProducts() {}
}
// Retrieve any key
const basePath = Metadata.get('basePath', ApiController); // '/api'
const cacheTtl = Metadata.get('cache:ttl', ProductController.prototype, 'getProducts'); // 3600
@InheritMetadata
Copy metadata from one class to another:
import { Meta, InheritMetadata, Metadata } from '@expressive-tea/commons';
// Base class with configuration
@Meta({ timeout: 5000, retries: 3, maxSize: 100 })
class BaseController {}
// Inherit specific keys
@InheritMetadata(BaseController, ['timeout', 'retries'])
class UserController {}
// Check inherited values
const timeout = Metadata.get('timeout', UserController); // 5000
const retries = Metadata.get('retries', UserController); // 3
const maxSize = Metadata.get('maxSize', UserController); // undefined (not inherited)
@CacheInMetadata
Cache method results in metadata:
import { CacheInMetadata, Metadata } from '@expressive-tea/commons';
class ConfigProvider {
@CacheInMetadata('computed:config')
getConfig() {
console.log('Computing expensive config...');
return {
apiKey: process.env.API_KEY,
timeout: 5000
};
}
}
const provider = new ConfigProvider();
provider.getConfig(); // Logs: "Computing expensive config..."
provider.getConfig(); // Returns cached value, no log!
// Access the cached value directly
const cached = Metadata.get('computed:config', ConfigProvider.prototype, 'getConfig');
@Deprecated
Mark classes or methods as deprecated:
import { Deprecated, Metadata } from '@expressive-tea/commons';
// Deprecate a class
@Deprecated('Use NewUserController instead')
class UserController {}
// Deprecate a method with warning
class ProductService {
@Deprecated('Use findById() instead', true)
getProduct(id: string) {
return this.findById(id);
}
findById(id: string) {
return { id, name: 'Product' };
}
}
// When called, logs: [DEPRECATED] ProductService.getProduct is deprecated. Use findById() instead
const service = new ProductService();
service.getProduct('123');
// Check if deprecated
const isDeprecated = Metadata.get('deprecated', UserController); // true
const message = Metadata.get('deprecated:message', UserController); // 'Use NewUserController instead'
Combining Decorators
Stack them up for maximum power!
import { Meta, SetMetadata, Deprecated, CacheInMetadata } from '@expressive-tea/commons';
@Meta({ type: 'controller', version: 'v2' })
@SetMetadata('basePath', '/api/v2')
class ApiController {
@SetMetadata('route:path', '/users')
@Meta({ method: 'GET', auth: true, rateLimit: 100 })
@CacheInMetadata('cache:users')
getUsers() {
return [];
}
@Deprecated('Use getUsers() with pagination instead')
getAllUsers() {
return this.getUsers();
}
}
API Reference
Metadata Class
The star of the show! All methods are static:
| Method | What It Does |
|---|---|
set(key, value, target, propertyKey?) |
Store metadata |
get(key, target, propertyKey?) |
Retrieve metadata |
has(key, target, propertyKey?) |
Check if metadata exists |
delete(key, target, propertyKey?) |
Remove metadata |
getKeys(target, propertyKey?) |
List all metadata keys |
getOwnKeys(target, propertyKey?) |
Get own keys (not inherited) |
getType(target, propertyKey) |
Get design-time type |
getParamTypes(target, propertyKey) |
Get parameter types |
getReturnType(target, propertyKey) |
Get return type |
Pro tip: Check out the JSDoc comments for detailed usage and examples!
Helper Functions
| Function | What It Does |
|---|---|
getClass(target) |
Get constructor from instance or prototype |
Development
Want to contribute? Awesome! Here's how to get started:
# Install dependencies
yarn install
# Build the package
yarn build
# Run tests
yarn test
# Watch mode for tests
yarn test:watch
# Lint your code
yarn lint
# Format your code
yarn format
Project Structure
@expressive-tea/commons/
โโโ src/
โ โโโ classes/
โ โ โโโ Metadata.ts # ๐ฏ The metadata powerhouse
โ โโโ helpers/
โ โ โโโ object-helper.ts # ๐ ๏ธ Handy utilities
โ โโโ types/
โ โ โโโ index.ts # ๐ TypeScript type definitions
โ โโโ __test__/
โ โ โโโ unit/ # ๐งช Comprehensive tests
โ โโโ index.ts # ๐ฆ Public API exports
โโโ dist/ # ๐๏ธ Compiled output (generated)
โโโ package.json
โโโ tsconfig.json
โโโ README.md
Contributing
We'd love your help making commons even better! Here's how:
- Fork the repo
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes and add tests (we love tests!)
- Run
yarn test- all green? Great! - Run
yarn lint- make it pretty - Commit:
git commit -m "feat: add amazing feature" - Push and create a PR
Quality checklist:
- Tests pass
- Linting passes
- Coverage stays high (>90%)
- JSDoc added for public APIs
- TypeScript compiles with no errors
Check out our Contributing Guide for more details!
Related Packages
- @expressive-tea/plugin - Plugin system built on commons
- @expressive-tea/core - The full framework
Changelog
See CHANGELOG.md for what's new in each release.
Migrating from v0.0.x?
Check out our Migration Guide - it's easier than you think!
Support
Need help? We've got you covered:
- Documentation
- Gitter Chat - Real-time help
- GitHub Issues - Report bugs
- GitHub Discussions - Ask questions
- Email - Direct support
License
Apache-2.0 License - see LICENSE for details.
Free to use, free to modify, free to distribute. Build something awesome!
Author
Zero One IT - https://zerooneit.com
Contributors
- Diego Resendez - Original Author - @chrnx-dev
See all amazing contributors who've helped build this!
Made with and by the Expressive Tea Team
Brew something amazing today!