Here's how to bootstrap and evolve your Express‑style TypeScript app step by step:
1. Project Initialization
Initialize your project and TypeScript entry point:
npm init -y
mkdir src
touch src/app.ts .env.local .gitignore
Your package.json
now has defaults, and you have:
-
src/app.ts
– your app entry -
.env.local
– development env file -
.gitignore
– to protect sensitive files
2. Environment Variables in .env.local
# .env.local
PORT=5000
This file sets PORT=5000
for development, allowing differentiation from production.
3. TypeScript & @types/node
Setup
Install type definitions for Node.js to fix lint issues like process.env.PORT
:
npm install -D @types/node
@types/node
provides TypeScript types for built-in Node APIs, improving IDE feedback.
4. Reading Env & Demo Outputs
// src/app.ts
const PORT = process.env.PORT;
console.log(`PORT is ${PORT}`); // undefined w/o loading env file
enum MyEnum { A, B }
console.log(MyEnum.A);
// Type stripping demo with Node
const calcSum = (a: number, b: number) => a + b;
const calc = calcSum(4, 10);
console.log(`The sum is ${calc}`);
- With
node src/app.ts
,.env.local
is not loaded, soPORT
isundefined
. - Node runs TS by stripping type annotations, even if types mismatch—it doesn't type-check at runtime.
5. Scripts & Loading .env.local
In package.json
:
"scripts": {
"dev": "node --env-file=.env.local src/app.ts"
}
-
npm run dev
now loads.env.local
, soPORT
logs5000
. - For production, switch to
.env
with"start": "node --env-file=.env src/app.js"
.
6. Safe .gitignore
.env
.env.local
node_modules
dist
This prevents secrets and build artifacts from leaking to GitHub, and node_modules
is rebuilt via npm install
, avoiding bloat.
7. Enable Watch Mode
Improve dev workflow:
"dev": "node --env-file=.env.local --watch src/app.ts"
- This watches for file changes without extra tools (no
nodemon
orts-node
).
8. Node’s Inline Type Stripping: Behavior
Node v23.6+ strips types automatically (no --experimental-strip-types
needed). E.g.,:
const calcSum = (a: number, b: boolean) => a + b;
console.log(calcSum(3, true)); // logs 4 — no type-checking
TypeScript errors won’t be caught at runtime—types serve only as editor aids.
9. Faster Dev with ts-node-dev
Install:
npm install -D ts-node-dev
Use in scripts:
"scripts": {
"dev": "tsnd --respawn src/app.ts"
}
-
ts-node-dev
keeps TypeScript compiler in memory and restarts faster thannode-dev
ornodemon
. - Remove
--transpile-only
to enable full type-checking before runtime. -
--respawn
ensures clean restarts on file change.
10. Build Output is In-Memory
Even with tsconfig.json
configured (rootDir: "src"
, outDir: "dist"
), ts-node-dev
runs in-memory—so no dist
folder appears unless you compile manually.
11. Bun—Fast, but Selective Support
You experimented with Bun:
"scripts": {
"dev": "bun --watch src/app.ts",
"check": "tsc --noEmit",
"build": "bun build src/app.ts --outdir dist",
"start": "bun run dist/app.js"
}
- Bun is extremely fast and first-class TS, but OS support is not as universal as Node.
- Like Node, Bun strips types only and relies on editor +
tsc
for type-checking.
Summary Workflow
Phase | Command | Notes |
---|---|---|
Dev | npm run dev |
Loads .env.local , watches files, no type-check |
Build | npm run check && tsc && bun build |
Type-check + compile to dist/
|
Production |
bun run dist/app.js or node dist/app.js
|
Run compiled code for consistency across OS |
TL;DR
- Use
.env.local
for dev,.env
for prod, and ignore both in Git. - Node/Bun auto-strip TS types—they don’t type-check at runtime.
- For speed: use
ts-node-dev
or Bun’s watch. - For safety: run
tsc --noEmit
during build or CI. - Choose Node as your runtime for maximum OS compatibility; Bun is optional for speed gains.
Top comments (0)