Skip to content
C Codeloom
Node.js

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.

·4 min read · By Codeloom
Beginner 9 min read

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)
Dev runs TS directly; production runs compiled JS.

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.