Right, let’s get our hands dirty. You’re not here to drag and drop files in some bloated IDE wizard that generates 50 files you don’t understand. We’re going to build this from the ground up, the way it should be: with intention and control. This is the foundation, and a shaky foundation here will cause migraines later. Trust me, I’ve been there.

First, make a directory for your project and cd into it. This isn’t a tutorial on using your terminal, but if you’re fuzzy on that, now’s the time to get cozy with it. You can’t avoid the command line in this line of work, and anyone who tells you otherwise is trying to sell you something.

The Starting Point: package.json

Every Node.js project, TypeScript or not, starts with a package.json file. It’s the manifest, the blueprint, the recipe card for your application. We’re going to create it with some sensible defaults.

mkdir my-awesome-ts-project
cd my-awesome-ts-project
npm init -y

The -y flag tells npm to just use the default values for everything. We’re going to change most of it anyway, so why bother answering its inane questions upfront? Now, open that newly created package.json. Let’s make it useful.

{
  "name": "my-awesome-ts-project",
  "version": "1.0.0",
  "description": "Because yet another TODO app won't build itself",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "author": "You (you magnificent developer)",
  "license": "MIT"
}

Notice the main point is now dist/index.js. That’s our first forward-looking change. We’re going to write code in src in TypeScript (.ts), and the TypeScript compiler will transpile it to plain JavaScript in the dist directory. The scripts section is where the magic happens. We’ve stubbed in build and start for later.

Installing the Key Players

Here’s where we bring in the heavy artillery. We need two things:

  1. TypeScript itself: The compiler, the brain, the whole point of this exercise.
  2. Type definitions for Node.js: This is the secret sauce. Node is written in JavaScript. TypeScript has no idea what process.argv or fs.readFile are by default. These type definitions (.d.ts files) tell TypeScript, “Hey, here’s the shape of the Node.js standard library.” It’s like giving a brilliant architect a detailed map of the existing infrastructure.

Run this command:

npm install --save-dev typescript @types/node

--save-dev (-D for short) is crucial here. These are development dependencies. Your production application doesn’t need the TypeScript compiler or the type definitions to run; it just needs the compiled JavaScript. Keeping them separate is a best practice that keeps your deployment lean and mean.

Configuring the Beast: tsconfig.json

You could run the TypeScript compiler (tsc) with command-line flags. You could also dig a foundation with a teaspoon. Don’t do that. We use a tsconfig.json file to dictate exactly how our project is compiled. Generate a starter one with:

npx tsc --init

This spits out a massive, commented-out file with every possible option. It’s overwhelming. Here’s the parts you actually need to care about for a standard Node.js project. Strip it down to something like this:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Let’s talk about the why:

  • target: We’re using a modern Node.js version, so we can target a modern ES version (ES2022). This lets the compiler output cleaner, more efficient code since it doesn’t have to polyfill features for ancient browsers.
  • module & moduleResolution: Setting this to NodeNext is the correct, modern way. It makes TypeScript understand Node’s new ES module system, saving you from a world of import/require confusion. This one change solves about 40% of “why won’t my import work?!” questions.
  • rootDir & outDir: This is our convention. Source code in src, compiled output in dist. Clean separation. The compiler will mirror the directory structure from src into dist.
  • strict: true: This is non-negotiable. This enables the full suite of type checks. Is it a pain at first? Absolutely. It will feel like the compiler is yelling at you for every little thing. That’s the point. It’s catching your mistakes before you run the code. This is the entire value proposition of TypeScript. Turning this off is like buying a sports car and leaving the parking brake on.

Your First TypeScript File

Let’s prove this works. Create a src directory and an index.ts file inside it.

// src/index.ts
function greet(name: string): string {
  return `Hello, ${name}! You magnificent developer, you.`;
}

const message: string = greet(process.argv[2] || "World");
console.log(message);

This is simple, but it’s already doing TypeScript things: type annotations on the function parameters and return value.

Building and Running

Now, let’s make it go. Run the build script we defined earlier:

npm run build

If you’ve done everything right, you’ll see a dist directory appear with an index.js file inside. Peek at it. It’s the compiled JavaScript, with all the types stripped out. Now, run it:

npm start
# Or be more specific:
node dist/index.js YourName

You should see your greeting. Congratulations, you’ve just run your first TypeScript Node.js project. The pipeline is working.

The Dev Loop: Watch and Clarity

Running npm run build every time you change a file is for chumps. Let’s add a couple more scripts to our package.json to make development actually pleasant.

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev:build": "tsc --watch",
    "dev:run": "nodemon dist/index.js"
  }
}

You’ll need to install nodemon for this: npm install --save-dev nodemon.

Now, in one terminal, run npm run dev:build. This will start the TypeScript compiler in watch mode; it will recompile every time you save a .ts file. In another terminal, run npm run dev:run. Nodemon will watch the dist directory and restart your Node app whenever a new .js file is output.

This is your development loop. You save your TypeScript, it automatically compiles, and your application automatically restarts. It feels like magic, but it’s just well-orchestrated tooling. This is how you build without going insane.