Skip to content
TypeScript ts node 3 min read

Setting Up TypeScript with Node.js

Running TypeScript on Node.js gives you a typed standard library, safer module boundaries, and editor intelligence for the entire server. The setup is small but worth getting right: install TypeScript and the Node type definitions, write a tsconfig.json tuned for a server runtime, and decide how you will compile and run the output. This page walks through a fresh project from npm init to a working build, and explains the compiler options that matter specifically for Node.

Installing the toolchain

Add TypeScript and the Node type definitions as dev dependencies. @types/node supplies the types for built-in modules like fs, path, http, and the global process object — without it the compiler does not know what Node provides.

npm init -y
npm install --save-dev typescript @types/node
npx tsc --init

Pin @types/node to your runtime: if you deploy on Node 22, install @types/node@22 so the typings match the APIs actually available. A mismatch lets you call functions in the editor that throw at runtime.

Check your Node version with node -v and match the major version of @types/node to it. The @types package tracks Node releases line by line.

A tsconfig for Node

tsc --init produces a heavily commented file. For a modern Node server, the options below are the ones that change behavior the most. ESM is the default for new projects, so target a current library and emit ES modules.

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "sourceMap": true,
    "declaration": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

module: "NodeNext" makes the compiler honour Node’s own ESM/CommonJS resolution rules, including the "type" field in package.json and the requirement that relative ESM imports carry a file extension. Pair it with moduleResolution: "NodeNext" — they are meant to be set together.

ESM vs CommonJS

Node supports both module systems, and your choice drives several tsconfig and source decisions. Set "type": "module" in package.json for ESM, or leave it out (or use "commonjs") for the older format.

ConcernESM ("type": "module")CommonJS
package.json type"module""commonjs" or omitted
Import syntaximport x from "./x.js"require("./x")
Relative import extensionRequired (.js)Optional
__dirname / __filenameUse import.meta.dirnameBuilt-in globals
module/moduleResolutionNodeNextNodeNext

A common surprise in ESM: relative imports must reference the compiled .js extension even in .ts source. import { db } from "./db.js" is correct; the compiler resolves it to db.ts but emits the .js path Node needs at runtime.

Project layout and scripts

Keep source under src/ and let the compiler emit to dist/. Wire up scripts so the build, type-check, and start commands are one word each.

// package.json
{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "typecheck": "tsc --noEmit",
    "start": "node dist/index.js",
    "dev": "tsx watch src/index.ts"
  }
}

build compiles src/ to dist/, typecheck validates types without writing files (ideal for CI), and start runs the compiled output. The dev script uses tsx to run TypeScript directly during development — covered in detail on the next page.

Using Node globals and built-ins

With @types/node installed, the standard library and process are fully typed. Use import type for type-only imports so they are erased from the output entirely.

import { readFile } from "node:fs/promises";
import type { Server } from "node:http";

const config = await readFile("config.json", "utf8");
console.log(process.env.NODE_ENV ?? "development");

// `node:` prefix makes built-in imports explicit and unambiguous

Prefer the node: protocol prefix (node:fs, node:path) for built-in modules. It signals intent clearly and prevents a local package from accidentally shadowing a core module.

Best Practices

  • Install @types/node and pin its major version to your deployment runtime.
  • Use module/moduleResolution: "NodeNext" so the compiler mirrors Node’s real resolution.
  • Enable strict from day one; retrofitting strictness later is far more painful.
  • Keep src/ and dist/ separate via rootDir and outDir, and gitignore dist/.
  • Add a tsc --noEmit typecheck script and run it in CI.
  • Prefer the node: import prefix for built-in modules to avoid shadowing.
  • In ESM, write relative imports with the .js extension that Node expects at runtime.
Last updated June 29, 2026
Was this helpful?