Upgrade Guide: v3 to v4
Migrate an ExpressoTS 3.x application to the 4.x line (4.0.0 and later).
Prerequisites
| Requirement | Version |
|---|---|
| Node.js | 20.19.0+ |
| Current app | ExpressoTS 3.x |
Breaking changes summary
Two things break when moving a v3 app to v4: a handful of public names were removed, and .env loading became explicit. Everything else is additive or opt-in.
Removed APIs and their replacements
| Removed in v4 | Use instead |
|---|---|
AppFactory / AppFactory.create(App) | await bootstrap(App) from @expressots/core |
InMemoryDataProvider, InMemoryDataTable | InMemoryDBProvider |
LazyServiceIdentifer (typo alias) | LazyServiceIdentifier |
AppContainer#viewContainerBindings() | AppContainer#introspect() or getFormattedBindingsView() |
Logger#formatMessage() (legacy) | The logger formats through transports; nothing to call |
AppFactory is the one most code, and most AI coding assistants, still reach for. Since 4.3.0 the name exists again only as a compile-time tombstone: AppFactory.create(...) fails to compile with an error that names the replacement, and throws the same message at runtime. Projects scaffolded by the CLI ship an AGENTS.md with these rules, and expressots llms prints them for the installed version.
v3:
import { AppFactory } from "@expressots/core";
import { App } from "./app";
AppFactory.create(App).then((app) => app.listen(3000));
v4:
import { bootstrap } from "@expressots/core";
import { App } from "./app";
void bootstrap(App); // App extends AppExpress; port from PORT or 3000
.env files are now opt-in (ADR-001)
In v3.x the framework called dotenv.config() automatically. In v4 you opt in by passing envFileConfig to bootstrap(). This was changed because containerised deployments (Docker, Kubernetes, serverless) inject env vars via process.env and a missing .env file used to trip alarms in production.
v3.x (implicit):
import { AppFactory } from "@expressots/core";
import { App } from "./app";
AppFactory.create(App).then((app) => app.listen(3000));
// .env was always loaded
v4 (explicit):
import { bootstrap } from "@expressots/core";
import { App } from "./app";
await bootstrap(App, {
envFileConfig: {
files: { development: ".env", production: ".env.prod" },
autoCreateTemplate: true,
},
});
If you do nothing, v4 will silently skip .env loading and rely on process.env only. CI environments are auto-detected (GitHub Actions, GitLab CI, Jenkins, generic CI=true).
Recommended alternative: call
loadEnvSync()(also from@expressots/core) directly insrc/main.tsbeforebootstrap(App). This is what theapplicationandapplication-with-eventstemplates do. It's an explicit, side-effect-free import that loads.env,.env.${NODE_ENV}, and the matching.localoverrides, with no extrabootstrap()config to remember:src/main.tsimport { bootstrap, loadEnvSync } from "@expressots/core";import { App } from "./app";loadEnvSync();void bootstrap(App);Use
envFileConfigonly when you need bootstrap-time validation (required,validateValues) orautoCreateTemplate.
For the full rationale see ADR-001 in expressots/packages/core/src/application/.docs/decision-log.md.
New in v4.0.0 (non-breaking)
Nothing in this section requires code changes. These are additive features you can adopt incrementally after the upgrade.
Studio is part of v4.0.0
@expressots/studio and @expressots/studio-agent ship alongside @expressots/core in the v4.0.0 release. Install both as dev dependencies:
npm install -D @expressots/studio @expressots/studio-agent
The agent auto-activates when NODE_ENV=development and the package is installed; no app.ts changes are needed. Production deployments pay zero runtime cost. See the Studio section for the full tour, including supply-chain and runtime posture security analysis.
Interceptors, events, lazy loading, content negotiation, advanced authorization
These are all new in v4.0.0. None of them require changes to a v3 app; you opt in when you want them. See the dedicated pages under Features and Guides.
Step 1: Update Dependencies
Update your package.json dependencies to the latest versions:
{
"dependencies": {
"@expressots/core": "^4.3.0",
"@expressots/adapter-express": "^4.3.0",
"@expressots/shared": "^4.3.0"
},
"devDependencies": {
"@expressots/cli": "^4.3.0"
}
}
Then run:
- npm
- yarn
- pnpm
npm install
yarn install
pnpm install
Step 2: Update Node.js Version
Ensure your Node.js version is 20.19.0 or higher:
node --version
Step 3: Application Changes
Bootstrap (required)
Replace AppFactory.create() with bootstrap(). It builds the container, runs the AppExpress lifecycle hooks, starts the server, and wires graceful shutdown; there is no separate listen() call.
Before (v3):
import { AppFactory } from "@expressots/core";
import { App } from "./app";
AppFactory.create(App).then((app) => app.listen(3000));
After (v4):
import { bootstrap } from "@expressots/core";
import { App } from "./app";
await bootstrap(App);
// Or with options
await bootstrap(App, {
port: 4000,
appName: "My API",
appVersion: "1.0.0",
});
Application class (no changes)
The AppExpress lifecycle keeps its v3 shape: configContainer(), globalConfiguration(), configureServices(), postServerInitialization(), serverShutdown(). Controllers, providers, modules and decorators are unchanged.
Step 4: New Features (Optional)
Interceptors
Add interceptors to your routes:
import { UseInterceptors, PerformanceInterceptor, LoggingInterceptor } from "@expressots/core";
@controller("/users")
export class UserController {
@Get("/")
@UseInterceptors(PerformanceInterceptor, LoggingInterceptor)
getUsers() {
return this.userService.findAll();
}
}
Event System
Create type-safe events:
// events/user.events.ts
export class UserCreatedEvent {
constructor(
public readonly userId: string,
public readonly email: string
) {}
}
// handlers/user-created.handler.ts
import { provide, OnEvent, IEventHandler } from "@expressots/core";
import { UserCreatedEvent } from "../events/user.events";
@provide(UserCreatedHandler)
@OnEvent(UserCreatedEvent)
export class UserCreatedHandler implements IEventHandler<UserCreatedEvent> {
handle(event: UserCreatedEvent) {
console.log(`User created: ${event.userId}`);
}
}
Testing Module
Use the new testing utilities:
import { createTestApp, request } from "@expressots/core";
describe("UserController", () => {
let app: any;
beforeAll(async () => {
const testApp = await createTestApp(App);
app = testApp.app;
});
test("GET /users", async () => {
await request(app)
.get("/users")
.expectStatus(200);
});
});
Enhanced Configuration
Use type-safe configuration:
import { defineConfig, Env } from "@expressots/core";
export default defineConfig({
database: {
url: Env.string("DATABASE_URL", { required: true }),
pool: Env.number("DB_POOL_SIZE", { default: 10 }),
},
});
Step 5: Testing
After upgrading, run your tests to ensure everything works:
- npm
- yarn
- pnpm
npm test
yarn test
pnpm test
Troubleshooting
Common Issues
-
Node.js Version Error
If you see errors about unsupported Node.js features, ensure you're using Node.js 20.19.0 or higher.
-
TypeScript Errors
Update your TypeScript to version 5.x or higher for best compatibility.
-
Dependency Conflicts
Clear your
node_modulesand lock file, then reinstall:rm -rf node_modules package-lock.jsonnpm install
Getting Help
If you encounter issues during the upgrade:
- Check the GitHub Issues
- Join our Discord community
- Read the documentation
Summary
| Step | Action |
|---|---|
| 1 | Update dependencies to the 4.x line |
| 2 | Ensure Node.js 20.19.0+ |
| 3 | Replace AppFactory.create() with bootstrap() and rename any other removed API from the table above |
| 4 | Decide how .env is loaded (loadEnvSync() or envFileConfig) |
| 5 | Run tests |
| 6 | Adopt new features (optional) |
Beyond the removed names and explicit .env loading, v4 is compatible with v3 application code. New features are additive and can be adopted gradually.