Chapter 22: TypeScript with Node.js
TypeScript with Node.js like we’re sitting together in a quiet Hyderabad café, laptop open, terminal ready, and no hurry at all. I’ll explain everything step-by-step, very practically, with real 2025–2026 patterns that people actually use in production (Express APIs, NestJS backends, CLI tools, serverless functions, etc.).
1. What does “TypeScript with Node.js” really mean?
- You write your backend / server / script / CLI code in TypeScript (.ts / .tsx files)
- You get static types, interfaces, generics, better refactoring, autocompletion, fewer runtime surprises
- At the end → TypeScript compiler (tsc) or a runner turns your .ts into plain JavaScript (.js) that Node.js can execute
- Node.js itself does not understand TypeScript — it only runs JavaScript (or increasingly experimental TypeScript in very recent versions)
In 2026, there are several realistic ways to run TypeScript code in Node.js:
| Way | How it works | Best for | Production ready? | Speed / DX in 2026 |
|---|---|---|---|---|
| Compile → run JS (tsc + node dist/index.js) | Classic: build step → output .js | Almost all production servers | ★★★★★ | Good build, good run |
| ts-node / tsx runner | Run .ts directly (transpiles on-the-fly) | Development, scripts, quick prototypes | ★★★★ | Very convenient |
| Node.js native –experimental-strip-types (Node 22+ / 23+) | Node runs .ts files almost natively (very new) | Very light dev setups | ★★☆ (2026 still experimental) | Fastest dev experience |
| bun / deno runtime | They understand TS out-of-the-box | If you’re open to non-Node runtimes | ★★★★ | Excellent DX |
Most serious production Node.js + TS projects in 2026 still use compile → run JS (way #1) because it’s stable, debuggable, works with every tool (Docker, PM2, Kubernetes, Vercel, etc.).
2. Modern setup in 2026 (ESM + TypeScript — recommended for new projects)
Let’s build a tiny Express API together — this is the most common real-world pattern right now.
Step 1: Initialize project
|
0 1 2 3 4 5 6 7 8 9 |
mkdir my-node-ts-api cd my-node-ts-api npm init -y |
Step 2: Install dependencies
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
# Core runtime + types npm install express npm install --save-dev typescript @types/node @types/express tsx # Optional but very common in 2026 npm install --save-dev eslint prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin npm install dotenv # for .env files |
package.json — make it ESM (very important in 2026)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ "name": "my-node-ts-api", "type": "module", // ← tells Node & tools → ESM "scripts": { "dev": "tsx watch src/index.ts", // fast dev with hot-reload "build": "tsc", "start": "node dist/index.js", "typecheck": "tsc --noEmit" } } |
Step 3: Create tsconfig.json (modern ESM-friendly 2026 settings)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
{ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2022", "module": "NodeNext", // or "nodenext" — strict ESM support "moduleResolution": "NodeNext", "lib": ["ES2022"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "./dist", "rootDir": "./src", "incremental": true, "isolatedModules": true, "verbatimModuleSyntax": true, // catches many import/export mistakes "noUncheckedIndexedAccess": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } |
Step 4: First file — src/index.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import express, { Request, Response } from 'express'; import dotenv from 'dotenv'; dotenv.config(); const app = express(); const port = Number(process.env.PORT) || 4000; app.use(express.json()); interface User { id: number; name: string; email: string; } const users: User[] = [ { id: 1, name: "Rahul", email: "rahul@example.com" }, { id: 2, name: "Priya", email: "priya@example.com" } ]; app.get('/api/users', (req: Request, res: Response) => { res.json(users); }); app.get('/api/users/:id', (req: Request<{ id: string }>, res: Response) => { const id = Number(req.params.id); const user = users.find(u => u.id === id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); }); app.listen(port, () => { console.log(`🚀 Server running at http://localhost:${port}`); }); |
Step 5: Run it
|
0 1 2 3 4 5 6 7 8 9 10 11 |
# Development (hot-reload with tsx) npm run dev # Production build + run npm run build npm start |
3. Why ESM + NodeNext in 2026?
- Better tree-shaking
- Top-level await support
- Consistent with frontend (Vite, Next.js, etc.)
- Future-proof (CommonJS is legacy now)
- But… requires explicit .js extensions in imports if you compile to JS:
|
0 1 2 3 4 5 6 7 8 9 10 |
// Correct in ESM + NodeNext import { something } from './utils.js'; // Wrong (will fail at runtime after compilation) import { something } from './utils'; |
Many teams use tsx in dev (it handles extensions automatically) and compile to .js for production.
4. Quick comparison: CommonJS vs ESM in Node + TS (2026 reality)
| Aspect | CommonJS (“module”: “commonjs”) | ESM (“module”: “NodeNext”, “type”: “module”) |
|---|---|---|
| Syntax | require(), module.exports | import, export |
| File extensions needed | No | Yes (in compiled JS) |
| Top-level await | No | Yes |
| New projects (2026) | Legacy / migration cases | Strongly recommended |
| Interop with old libs | Easier | Sometimes needs await import() |
| tsconfig setting | “module”: “commonjs”, “moduleResolution”: “node” | “module”: “NodeNext”, “moduleResolution”: “NodeNext” |
Recommendation 2026: New project → ESM + NodeNext Legacy / quick migration → CommonJS
5. Very common real patterns in 2026 Node + TS projects
- Environment variables → dotenv + zod for validation
- Error handling → Custom AppError class + middleware
- Async/await everywhere → express-async-errors or try/catch wrappers
- Type-safe routes → Libraries like express-zod-api, tRPC, NestJS
- Testing → vitest or jest + @types/jest
- Linting → ESLint flat config + Prettier + typescript-eslint
6. Mini homework (try today!)
- Create the project above
- Add one POST route /api/users that accepts JSON → validates with interface → adds to array
- Try npm run dev → add a user → GET /api/users in browser/Postman
- Intentionally pass wrong type → see TypeScript error before runtime
Any part confusing? Want to zoom into:
- Full NestJS + TypeScript setup
- tRPC / Fastify instead of Express
- Docker / deployment tips
- Handling ESM pain points (extensions, dual packages)
- Strict mode flags for backend safety
Just tell me — we’ll go deeper right there! 😄
