Skip to main content
Version: 4.0.0-preview

Running the App

Four CLI commands cover the full local lifecycle: write code with dev, compile with build, run the compiled output with prod, and (optionally) do the whole thing inside Docker with container-dev.

CommandWhat it does
devHot-reload dev server via tsx watch
buildtsc -p tsconfig.build.json; rewrites alias imports in opinionated projects
prodnode against the compiled JavaScript
container-devDocker-compose-based development workflow (start, stop, attach, shell, status, logs)

All four read expressots.config.ts from the project root and the compile path from tsconfig.build.json.

Prerequisites

FilePurpose
expressots.config.tsProvides entryPoint and opinionated flags.
tsconfig.build.jsonMust define compilerOptions.outDir. Used by build (output) and prod (input).

dev

Hot-reload local dev server.

expressots dev

Under the hood it shells out to:

tsx watch --clear-screen=false \
--exclude "**/dist/**" --exclude "**/build/**" \
--exclude "**/coverage/**" --exclude "**/*.generated.*" --exclude "**/*.log" \
./src/<entryPoint>.ts
  • tsx watch is tsx's built-in watch mode with no nodemon, no SIGTERM quirks on Windows. Reloads are sub-second.
  • Path aliases just work. tsx resolves @useCases/..., @providers/... etc. straight from tsconfig.json (even without a baseUrl), so there is no tsconfig-paths/register preload. Production gets the same aliases rewritten to relative paths at build time.
  • Logs persist across reloads. --clear-screen=false keeps tsx's Restarting... <file> line visible, so you always see what triggered a reload.
  • Noise is excluded. Generated and output files (dist/, build/, coverage/, *.generated.*, *.log) are kept out of the watcher; node_modules and dotfiles like .env* are ignored by tsx already.
  • The process inherits stdio; press Ctrl+C to stop. Saved files trigger a fast restart.

File watching on Windows and synced/network drives

On Windows, OneDrive, mapped network drives, or VM-shared folders, native OS file-watch events can be unreliable. This shows up as missed reloads or a server that "keeps restarting." If you hit that, switch the watcher to polling:

EXPRESSOTS_WATCH_POLL=1 expressots dev
Env varDefaultDescription
EXPRESSOTS_WATCH_POLL(unset)Set to 1 / true to poll the filesystem instead of using native events. Steadier on Windows and synced/networked drives, at a small CPU cost.
EXPRESSOTS_WATCH_INTERVAL300Poll interval in milliseconds (only used when polling is enabled).

Options

FlagAliasDefaultDescription
--container-cfalseRun dev inside Docker. Generates Dockerfiles + compose if missing.
--build-bfalse(With --container) Rebuild the dev container before starting.
--detach-dfalse(With --container) Run the dev container in the background.
expressots dev --container --build --detach

When --container is set, the CLI invokes Compose v2 (docker compose) when available and falls back to Compose v1 (docker-compose). Both Dockerfile.development and docker-compose.development.yml are generated on demand.

build

Compile to JavaScript.

expressots build

Under the hood:

npx tsc -p tsconfig.build.json

Then, for opinionated projects only, the CLI post-processes every emitted .js file:

  • Replaces require("@alias/<path>") imports with relative paths so the compiled bundle runs without tsconfig-paths/register.
  • Copies package.json into outDir so production tooling (npm prune --production, npm start, etc.) can resolve dependencies.
Why the rewrite

v3 required register-path.js at runtime. v4 removed that. Alias resolution is baked into build time, so production startup has zero path-mapping cost.

prod

Run the compiled output with plain Node.

expressots prod

The exact invocation depends on whether the project is opinionated:

opinionatedCommand
truenode ./${outDir}/src/${entryPoint}.js
falsenode ./${outDir}/${entryPoint}.js

In either case the alias rewrite has already happened during build, so production startup is just node <file>.

container-dev

Compose-driven development workflow. Use this when your dev story requires sidecar services (Postgres, Redis, RabbitMQ) and you want them all running on docker compose up.

ActionPurpose
start(default) Boot the dev compose file in the foreground.
stopStop the dev compose stack.
attachAttach to the app container's stdout.
shellOpen an interactive shell inside the app container.
statusShow running container state for the compose project.
logsTail the app container's logs.
expressots container-dev start --build
expressots container-dev logs --follow --tail 200
expressots container-dev shell
expressots container-dev stop

Options

FlagAliasDefaultDescription
--container-cfalseForce the container path. Without this, start only prints guidance.
--service-sappService name from the compose file.
--compose-file-fdocker-compose.development.ymlCompose file to use.
--build-bfalseRebuild images on start.
--detach-dfalseRun start in the background.
--port-p(compose-driven)Override the host port for start.
--debug-port9229Expose the Node inspector on this port.
--watch-wtrueMount source for live edits.
--followtruelogs follow mode.
--tail100logs tail line count.
dev or container-dev?
  • expressots dev is faster. tsx watch reload is sub-second and there is no Docker layer.
  • expressots dev --container is a one-shot containerized dev.
  • expressots container-dev is the structured Compose workflow when you need multiple services and a long-lived dev environment.

Common scripts

expressots new emits these scripts in package.json:

{
"scripts": {
"dev": "expressots dev",
"build": "expressots build",
"prod": "expressots prod"
}
}

So you can call them through your package manager too:

npm run dev
npm run build
npm run prod

Troubleshooting

ProblemFix
tsconfig.build.json: outDir missingAdd "outDir": "build" (or any folder name) under compilerOptions.
Cannot find module '@useCases/...' in productionYou ran node directly. Use expressots prod so the alias rewrite is applied, or rebuild first.
Port already in use (EADDRINUSE)Stop the previous process or change the port via env / bootstrap({ port }).
Dev server "keeps restarting" on WindowsRun with EXPRESSOTS_WATCH_POLL=1 to switch the watcher to polling. The always-visible Restarting... <file> line tells you which file is triggering it (often a generated file or an editor's atomic save).
--container first run is very slowExpected. It generates Dockerfile.development, builds the image, and pulls base layers. Subsequent runs are cached.