Node.js TypeScript Setup Tutorial
Set up a clean, modern TypeScript project for Node.js with tsconfig, build scripts, ESM, and a dev loop that does not get in your way.
What you'll learn
- ✓How tsc fits into Node.js
- ✓Key tsconfig options
- ✓ESM vs CommonJS
- ✓Fast dev with tsx
- ✓Build and run in production
Prerequisites
- •Node.js installed
- •Familiarity with npm
What and Why
TypeScript gives JavaScript a static type system. In Node.js projects, that pays off most when teams grow, modules multiply, and refactors become risky. Types catch a class of bugs before runtime, make autocomplete useful, and document intent in a way comments cannot.
The cost is a build step and some configuration. Modern tooling has made both small, but only if you set them up clearly. A messy TypeScript project tends to fight you on imports, paths, and module formats. A clean one disappears into the background.
Mental Model
There are two phases: type checking and emitting JavaScript. The TypeScript compiler tsc can do both. In dev, you often skip emitting and run TypeScript directly with a loader like tsx. In production, you emit JavaScript ahead of time and run plain Node on the output.
Your tsconfig.json controls both phases. The most important questions are: which module system, which target Node version, where the source lives, where the output goes, and how strict the type checker is.
Hands-on Example
A minimal ESM TypeScript project that runs in dev and builds for production.
// package.json
{
"name": "my-service",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.5.0",
"tsx": "^4.0.0",
"@types/node": "^22.0.0"
}
}
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"sourceMap": true
},
"include": ["src"]
}
// src/index.ts
const port = Number(process.env.PORT ?? 3000);
console.log(`listening conceptually on ${port}`);
npm run dev gives you a fast watcher, npm run build produces JavaScript in dist/, and npm start runs it under plain Node.
Dev:
src/*.ts --(tsx watch)--> Node runtime
Production:
src/*.ts --(tsc build)--> dist/*.js --(node)--> runtime
|
+--> typecheck in CI (tsc --noEmit) Common Pitfalls
Mixing ESM and CommonJS without "type": "module" set correctly leads to confusing import errors. Pick one early and stick with it; new projects should pick ESM.
Forgetting .js extensions in ESM imports. With NodeNext, you import ./foo.js even though the source file is ./foo.ts. The extension refers to the compiled output.
Turning off strict to silence errors. Strict mode is what makes TypeScript actually useful; disabling it gives you all the build overhead with little benefit.
Shipping ts-node or tsx to production. Those are dev tools. Production should run pre-compiled JavaScript so startup is fast and the surface is small.
Practical Tips
Add a typecheck script and run it in CI separately from your build. That keeps fast iteration in dev and strict guarantees on merge.
Use @types/node matching your runtime. Mismatched versions cause confusing API gaps in autocomplete.
Configure path aliases sparingly. They are convenient but they require extra setup in tests and bundlers; relative imports are often less trouble.
Keep tsconfig small. A few well-chosen options beat a wall of flags copied from a tutorial. Each option should answer a real question your project has.
For libraries you publish, also emit .d.ts files and set types in package.json so consumers get types out of the box.
Wrap-up
A good TypeScript setup for Node.js is small: a strict tsconfig, ESM with NodeNext, tsx for dev, plain node for production, and a CI typecheck. Get those right and TypeScript stops being a chore. You gain refactor confidence and accurate autocomplete without the toolchain ever being the loudest thing in your day.
Related articles
- Node.js Node.js Zod Validation Tutorial
Use Zod to validate and infer types for request payloads, environment variables, and external data in Node.js apps.
- JavaScript JavaScript Modules: ESM vs CommonJS
A practical comparison of ES Modules and CommonJS: how they load, how they differ, interop strategies, and how to choose for a new project.
- TypeScript TypeScript Strict Mode and tsconfig Tips
Understand what strict mode actually turns on, which extra flags are worth enabling, and how to adopt strictness incrementally.
- Node.js Node.js Async Iterators Tutorial
Master async iterators in Node.js for streaming files, paginated APIs, and backpressure-aware data processing.