First steps
This page walks you from a clean machine to a running ExpressoTS v4 server. Every command and file shown here matches what expressots new actually produces: the exact same scaffolds the framework's own validation suite runs against.
Architecture
ExpressoTS centers on a DI container that wires controllers, use cases, providers, and repositories through modules. Incoming requests flow through controllers, which delegate to use cases and external providers as needed.

- DTO IN/OUT: Structure for incoming and outgoing request data.
- Controller: Handles HTTP routing and request processing.
- Use Case: Executes application-specific business logic.
- Provider: Supplies external capabilities such as database access.
- Repository: Manages direct persistence communication.
Prerequisites
- Node.js
>= 20.18.0(LTS or newer). - npm, yarn, pnpm, or bun.
- Git (the CLI uses
degitto fetch templates from GitHub).
Verify your version:
node --version
# v20.18.0 or higher
Install the CLI
npm i -g @expressots/cli
While v4 is in preview, install the
nextchannel:npm i -g @expressots/cli@next.
Confirm the install:
expressots --help
Pick a template
ExpressoTS v4 ships four first-class templates. Pick the one that matches your use case: you can change later, nothing is locked in.
| Template | When to use it | Generated by |
|---|---|---|
application | The default REST/GraphQL API. Full DI container, controllers, middleware presets, lifecycle hooks. | expressots new <name> -t application -p npm (defaults to -s api when omitted) |
application-with-events | Same as application plus the type-safe Event Bus example wired in. | expressots new <name> -t application -p npm -e (defaults to -s api when omitted) |
micro | Single-file lightweight HTTP service, no DI container. Ideal for serverless functions and tiny gateways. | expressots new <name> -t micro -p npm |
provider | A reusable @expressots/*-style npm package. Dual ESM + CJS build, ready to publish. | expressots create --provider <name> |
- Silent (CI-friendly): pass both
-tand-ptogether. Example:expressots new my-api -t application -p npm -s api. - Preset default: when
-sis omitted in silent mode with theapplicationtemplate, the CLI defaults to-s api(the recommended preset). You can still pass any other preset explicitly, e.g.-s graphql. - Interactive: run
expressots new my-apiwith no-tor-p. The CLI prompts for template, package manager, preset, and whether to include events. - Events:
-eis a boolean switch only (no value). Use-eor--eventsto scaffoldapplication-with-events. Do not pass-e interactive; that string is parsed as a positional argument and fails validation.
The ex command is an alias for expressots (same binary).
When you scaffold an application (or application-with-events), the CLI also asks for a middleware preset:
Preset (-s flag) | What it includes |
|---|---|
api (default) | parse + logger + security (helmet) + compress + rate limit. The recipe behind Middleware.applyPreset("api"). |
web | The api preset plus cookies and session support. |
graphql | Tuned for GraphQL: parse + helmet + compression + an Apollo Server placeholder. |
microservice | Minimal request parsing + compression for service-to-service traffic. |
minimal | Just parse(). You wire the rest yourself. |
Scaffold a project
- application
- application-with-events
- micro
- provider
expressots new my-api -t application -p npm -s api
cd my-api
npm run dev
my-api/
├── src/
│ ├── main.ts # loadEnvSync() + bootstrap(App)
│ ├── app.ts # AppExpress class with all four lifecycle hooks
│ └── app.controller.ts # Welcome + /health route
├── test/
│ └── app.controller.spec.ts # createTestApp + fluent request DSL
├── .env # Copy of .env.example (PORT, NODE_ENV, LOG_LEVEL, …)
├── .env.example
├── expressots.config.ts # CLI scaffolding config (Pattern + scaffoldSchematics)
├── jest.config.ts
├── tsconfig.json
├── tsconfig.build.json
└── package.json
expressots new my-api -t application -p npm -s api -e
cd my-api
npm run dev
Same layout as application, plus an events/ folder with a working sample:
my-api/src/
├── main.ts
├── app.ts # configureServices() also runs setupEventSystemForExpress
├── app.controller.ts
└── events/
├── user-created.event.ts # Typed event payload
└── welcome-email.handler.ts # @OnEvent class-level handler, auto-discovered
expressots new my-svc -t micro -p npm
cd my-svc
npm run dev
my-svc/
├── src/
│ └── api.ts # micro() factory, two routes, error handler, listen()
├── test/
│ └── api.spec.ts # Spins up micro() on an OS-assigned port
├── examples/ # Reference snippets (serverless, circuit breaker, mesh)
├── .env
├── expressots.config.ts
├── jest.config.ts
├── tsconfig.json
├── tsconfig.build.json
└── package.json
expressots create --provider my-provider
cd my-provider
npm install
npm test
npm run build
my-provider/
├── src/
│ ├── index.ts # Public re-exports (uses .js extensions for ESM)
│ └── greeter.provider.ts # Sample @provide() class
├── test/
│ └── greeter.provider.spec.ts
├── scripts/
│ ├── build-esm.js # Atomic ESM build (sets "type":"module" temporarily)
│ ├── copy.js
│ └── rm.js
├── tsconfig.json # Base config (extended by cjs/esm)
├── tsconfig.cjs.json # module: commonjs → ./lib/cjs
├── tsconfig.esm.json # module: NodeNext → ./lib/esm/*.mjs
├── package.json # exports map: import → .mjs, require → .js
└── README.md
Run it
Once npm run dev is up, you'll see the ExpressoTS startup banner:
ExpressoTS v4.0.0 my-api v1.0.0 Node v22.x (linux)
⚡ Server ⚙️ Config 💚 Health
Env: development Prefix: /api Memory: 42 MB
Port: 3000 Node Version: v22.x Heap: 54 %
📊 Metrics 🛡️ Security ⏱️ Startup
Routes: 2 Interceptors: 0 Time: 36 ms
Controllers: 1 URL: localhost:3000
Providers: 1
Middleware: 7
Hit the routes:
curl http://localhost:3000/api/
# {"message":"Hello from ExpressoTS v4!","docs":"https://expresso-ts.com/docs/"}
curl http://localhost:3000/api/health
# {"status":"ok","uptime":4.46}
The global prefix
/apicomes from the template'sglobalConfiguration()hook (this.setGlobalRoutePrefix("/api")). Remove that line to expose routes at/.
The micro template skips the banner and global prefix: routes live at http://localhost:3000/.
What bootstrap() and loadEnvSync() do
src/main.ts in every application variant looks like this:
import { bootstrap, loadEnvSync } from "@expressots/core";
import { App } from "./app";
loadEnvSync();
void bootstrap(App);
loadEnvSync()is the recommended way to load.envfiles. Call it beforebootstrap()so process.env is fully populated before any config or DI binding runs. By default it loads.env, then layers.env.${NODE_ENV}and the matching.localoverrides on top: no arguments needed.bootstrap(App)instantiates theAppExpresssubclass, runs the lifecycle hooks (globalConfiguration→configureServices→postServerInitialization), starts the HTTP server, and wires up graceful shutdown onSIGINT/SIGTERM.
If you don't call loadEnvSync() and don't pass an envFileConfig option to bootstrap(), no .env files are read: only the variables already in process.env (the deployment platform, Docker secrets, CI runner, etc.). This is intentional: it keeps containerized and CI deployments fast and predictable. See Bootstrap → Environment file loading for the full opt-in matrix.
What app.ts does
src/app.ts extends AppExpress and overrides the four lifecycle hooks. The scaffold pre-fills the most common pieces:
import { AppExpress } from "@expressots/adapter-express";
import { AppContainer, CreateModule } from "@expressots/core";
import { AppController } from "./app.controller";
export class App extends AppExpress {
private readonly container: AppContainer = this.configContainer([
CreateModule([AppController]),
]);
globalConfiguration(): void {
this.setGlobalRoutePrefix("/api");
}
async configureServices(): Promise<void> {
this.Middleware.applyPreset("api"); // <- replaced by your -s preset
}
async postServerInitialization(): Promise<void> {}
async serverShutdown(): Promise<void> {}
}
| Hook | When it runs | Typical use |
|---|---|---|
globalConfiguration | Once, before DI is built | setGlobalRoutePrefix, banner customisation |
configureServices | Once, after DI is built, before listen() | Middleware.applyPreset(…), setupInterceptorsForExpress(…), setupEventSystemForExpress(…), setupAuthorizationForExpress(…) |
postServerInitialization | Once, after listen() resolves | warm caches, log readiness, register health probes |
serverShutdown | On SIGINT / SIGTERM | drain queues, close DB connections, flush logs |
See Lifecycle Hooks for a deep dive.
Daily commands
| Command | What it does |
|---|---|
npm run dev | Start dev server (tsx --watch, Studio agent enabled). |
npm run build | Compile to dist/, rewrite path aliases. |
npm run prod | Run the compiled output with node. |
npm test | Jest, single worker (--runInBand). |
npm run test:cov | Jest with coverage. |
npm run lint | ESLint with --fix. |
npm run format | Prettier write. |
npm run studio | Launch ExpressoTS Studio. |
Where to next
- Bootstrap: every option
bootstrap()accepts, plus the env-file flow. - Lifecycle Hooks: the four hooks plus provider-level
IBootstrap/IShutdown. - Configuration:
defineConfig+Env.*builders for typed env vars. - Middleware: built-in presets, custom middleware, profiler.
- Testing:
createTestApp, fluent HTTP, snapshots, custom matchers. - Studio: the in-development developer experience platform.
Support the project
ExpressoTS is MIT-licensed open source. If it saves you time, please support the project.