DEEP DIVE • @expressive-tea/core v2.0 • March 2026
What’s New in Expressive Tea 2.0: The Full Breakdown
Everything that changed, why it changed, and how to use it
I shipped the announcement already — Expressive Tea 2.0 is out. This post is the deep dive: what actually changed under the hood, the reasoning behind the breaking changes, and a walkthrough of every major new feature.
Let’s get into it.
The Security Rewrite
Let’s start with the reason this release exists at all. The v1.3.x beta had a critical cryptography vulnerability in the Teapot/Teacup remote gateway system. The original implementation had flaws in how it handled encryption — the kind of flaws that make security researchers wince.
v2.0 replaces the entire crypto layer:
• Weak key derivation
• Missing authentication on ciphertext
• Predictable IV generation
• AES-256-GCM authenticated encryption
• HKDF key derivation
• Cryptographically secure random IVs
Express 5 + InversifyJS 7
The two biggest runtime dependencies both got major version bumps.
Express 5 has been in development for years, and it finally landed with proper async error handling, improved routing, and a cleaner middleware pipeline. For Expressive Tea users this means async route handlers just work — no more wrapping everything in try/catch to prevent unhandled rejections.
InversifyJS 7 brings a reworked container architecture. We rebuilt the DI layer on top of it with proper scoped injection:
// Scoped DI — services live within their module boundary
@Module({
controllers: [UserController],
providers: [
{ provide: UserService, scope: 'singleton' },
{ provide: RequestLogger, scope: 'transient' }
],
mountpoint: '/api/users'
})
class UserModule {}
Singleton, Transient, and Scoped lifetimes are all supported now. Previously everything was effectively singleton — which worked until you needed request-scoped services. That limitation is gone.
TypeScript Strict Mode
The entire codebase now compiles under strict: true. This was painful to migrate — every any type, every implicit null, every untyped callback had to be fixed. But the result is a framework where the type system actually catches real bugs.
For your own code, this means:
- Decorator metadata plays nicely with strict mode
- All public APIs have explicit, narrow types
- Generic utilities like
Constructor<T>andTFunctionare properly constrained - No more
@ts-ignorein framework internals
TypeScript 5.0+ is required. We test against 5.9.
Built-in Health Checks
If you’re deploying to Kubernetes (or anything with health probes), this is for you. v2.0 ships with a built-in health check system:
@HealthCheck({
checks: [
{
name: 'database',
check: async () => {
const ok = await db.ping();
return { status: ok ? 'pass' : 'fail' };
},
critical: true,
timeout: 5000
},
{
name: 'redis',
check: async () => ({
status: redis.isReady ? 'pass' : 'warn'
}),
critical: false
}
]
})
class App extends Boot {}
Three endpoints out of the box:
GET /health
Full health report with all check results, response times, and metadata.
GET /health/live
Liveness probe. Returns 200 if the process is running. For K8s livenessProbe.
GET /health/ready
Readiness probe. Returns 503 if any critical check fails. For K8s readinessProbe.
No more rolling your own health endpoints or bolting on third-party middleware for basic operational checks.
Environment Variables Done Right
The new @Env decorator handles .env files natively:
@Env({ path: '.env', required: ['DATABASE_URL', 'API_KEY'] })
@Env({ path: '.env.local', override: true, silent: true })
@ServerSettings({ port: 3000 })
class App extends Boot {}
Multiple .env files, required variable validation, silent failures for optional files, and override control. No more installing dotenv separately and calling config() before anything else.
And starting in v2.0.1, you get type-safe environment variables with transform support:
import { z } from 'zod';
const EnvSchema = z.object({
PORT: z.string().transform(Number),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32)
});
type Env = z.infer<typeof EnvSchema>;
@Env<Env>({
transform: (env) => EnvSchema.parse(env),
onTransformError: 'throw'
})
class App extends Boot {
constructor() {
super();
const env = Settings.getInstance().getEnv<Env>();
console.log(env.PORT); // number — validated and transformed
}
}
Bring your own validation library — Zod, Joi, whatever. The framework doesn’t care what you use, it just calls your transform function and catches errors.
Configuration Files
v2.0.1 also adds YAML/JSON configuration file support:
# .expressive-tea.yaml
port: 3000
securePort: 4443
database:
host: localhost
port: 5432
cache:
enabled: true
ttl: 3600
File priority: .expressive-tea.yaml > .expressive-tea.yml > .expressive-tea (JSON). The framework picks up the first one it finds.
The Engine System
The boot system got clearer documentation and better error handling, but the architecture didn’t change fundamentally. Quick refresher:
BOOT LIFECYCLE
BOOT_DEPENDENCIES → BOOT_INITIALIZATION → APPLICATION → AFTER_APPLICATION_MIDDLEWARES → ON_HTTP_CREATION → START
Five engines ship by default:
| Engine | Purpose | When it runs |
|---|---|---|
| HTTPEngine | HTTP/HTTPS serving, module mounting | Always |
| SocketIOEngine | Socket.IO real-time | Always |
| WebsocketEngine | Raw WebSocket server | Conditional |
| TeapotEngine | Remote server gateway | Conditional |
| TeacupEngine | Remote server connector | Conditional |
Engines run init() in reverse order and start() in forward order. This lets dependent engines initialize before the engines that need them start listening.
Tooling Modernization
Testing: Jest → Vitest
Faster test runs, native ESM support, better TypeScript integration. Same test patterns, way less configuration.
Linting: ESLint v9
Migrated to flat config. Extends eslint-config-love with strict TypeScript rules. No more cascading .eslintrc files.
Package: Yarn 4
PnP-ready, faster installs, better monorepo support. The packageManager field locks to yarn@4.11.0.
Coverage sits at 95%+ across the board. Every PR runs the full suite in CI.
The New Package Namespace
The package moved from @zerooneit/expressive-tea to @expressive-tea/core. This wasn’t just cosmetic — the whole ecosystem now lives under @expressive-tea/*:
@expressive-tea/core— the framework@expressive-tea/commons— shared constants, types, metadata keys@expressive-tea/plugin— plugin development kit@expressive-tea/metadata— decorator metadata utilities
- "@zerooneit/expressive-tea": "^1.2.0"
+ "@expressive-tea/core": "^2.0.0"
Imports stay the same internally. The legacy package gets security patches until April 30, 2026 — after that, it’s archived.
Performance
A few things got faster by removing dependencies:
- Lodash utilities replaced with native ES2017+ equivalents where possible
- Chalk 5.x (pure ESM, smaller bundle)
- Reflect-metadata 0.2.2 (lighter than 0.1.x)
- Removed unused transitive dependencies
The framework itself isn’t in the hot path — Express does the heavy lifting at request time. But startup time and memory footprint both improved measurably.
Migration Checklist
If you’re upgrading from v1.x, here’s the short version:
- Update Node.js to 20+ (22 recommended)
- Update package name in
package.jsonto@expressive-tea/core - Update TypeScript to 5.0+ and enable
strict: true - Update Express to 5.x
- Re-encrypt any Teapot/Teacup data (if using remote gateway)
- Migrate ESLint to v9 flat config (if using framework’s lint setup)
- Run tests — fix any strict mode type errors
The full migration guide covers edge cases and has before/after examples for every breaking change.
What’s Next
v2.0 is the foundation. Here’s what’s in the pipeline:
- CLI scaffolding tool (
tea-cli) aligned with v2 patterns - First-party plugins for common use cases (auth, CORS, rate limiting)
- Documentation site overhaul
- More examples and starter templates
The framework is open source under Apache-2.0. If this sounds useful to you, try it out and let me know what breaks. Bug reports are welcome, PRs even more so.
Start building with Expressive Tea 2.0
Install the framework, read the docs, or check out the source.


Leave a Comment