Core utilities and shared types for Expressive Tea framework
npm install @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 โ
---
> [!IMPORTANT]
> ### ๐ Version Compatibility
>
> This package (v2026.1.0) requires @expressive-tea/core >= 2.0.0
>
> - โ
Compatible: @expressive-tea/core@2.x.x and above
> - โ Not compatible: @expressive-tea/core@1.x.x or below
>
> Using 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.
---
> [!NOTE]
> ### ๐ฆ 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/metadata for backward compatibility:
> - Metadata class (metadata management)
> - @SetMetadata, @Meta, @InheritMetadata, @CacheInMetadata, @Deprecated decorators
> - getClass helper utility
>
> For new projects, consider using @expressive-tea/metadata directly for better tree-shaking and smaller bundle sizes.
>
> For existing projects, no changes needed - everything works the same way!
---
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.
- โจ 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 any nightmares 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!
``bashnpm
npm install @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!):`json
{
"compilerOptions": {
"experimentalDecorators": true, // ๐จ Enable decorator magic
"emitDecoratorMetadata": true, // ๐ Emit design-time type info
"target": "ES2017",
"module": "commonjs"
}
}
`๐จ Usage Examples
$3
Let's build a
@Route decorator from scratch:`typescript
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!
$3
Decorators aren't just for methods - classes can have metadata too:
`typescript
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']
`$3
Need to inject dependencies? Property metadata has your back:
`typescript
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' }
// ]
`$3
Level up with parameter metadata:
`typescript
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' }
`$3
TypeScript can tell you the types at design time:
`typescript
import { Metadata } from '@expressive-tea/commons';class BlogService {
createPost(title: string, content: string): Promise {
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
`$3
`typescript
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.
$3
Set metadata with a single decorator:
`typescript
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'
`$3
Set multiple metadata key-value pairs at once:
`typescript
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
`$3
Copy metadata from one class to another:
`typescript
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)
`$3
Cache method results in metadata:
`typescript
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');
`$3
Mark classes or methods as deprecated:
`typescript
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'
`$3
Stack them up for maximum power! ๐
`typescript
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
$3
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!
$3
| Function | What It Does |
|----------|-------------|
|
getClass(target) | ๐๏ธ Get constructor from instance or prototype |๐ ๏ธ Development
Want to contribute? Awesome! Here's how to get started:
`bash
Install dependencies
yarn installBuild the package
yarn buildRun tests
yarn testWatch mode for tests
yarn test:watchLint your code
yarn lintFormat 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:
1. ๐ด Fork the repo
2. ๐ฟ Create a feature branch:
git checkout -b feature/amazing-feature
3. โ๏ธ Make your changes and add tests (we love tests!)
4. โ
Run yarn test - all green? Great!
5. ๐จ Run yarn lint - make it pretty
6. ๐พ Commit: git commit -m "feat: add amazing feature"`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!
- @expressive-tea/plugin - Plugin system built on commons
- @expressive-tea/core - The full framework
See CHANGELOG.md for what's new in each release.
Check out our Migration Guide - it's easier than you think!
Need help? We've got you covered:
- ๐ Documentation
- ๐ฌ Gitter Chat - Real-time help
- ๐ GitHub Issues - Report bugs
- ๐ก GitHub Discussions - Ask questions
- ๐ง Email - Direct support
Apache-2.0 License - see LICENSE for details.
Free to use, free to modify, free to distribute. Build something awesome! ๐
Zero One IT - https://zerooneit.com
- 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!