# Setting up Express.js with TypeScript

![](https://cdn.hashnode.com/uploads/covers/6185effafd5d634d0169926f/b1b679a4-3756-454a-8940-878311ea9e73.png align="center")

  
**Audience**: Developers familiar with Node.js basics. No prior TypeScript experience required.

**TypeScript** adds static typing and better tooling to Express. This guide sets up a production-ready Express + TypeScript project from scratch — with separate `dev` (hot-reload) and `build` (compiled JS) workflows.

* * *

## Why this setup matters

Running Express in plain JavaScript means type errors surface at runtime — often in production. TypeScript catches them at compile time. More concretely: without `@types/express`, your IDE has no idea what `req.body` or `res.status()` returns. With it, autocomplete works, typos fail loudly, and refactors are safer.

The tradeoff: a slightly more involved project structure. This guide handles that once so you don't have to think about it again.

* * *

## Prerequisites

*   Node.js ≥ 18
    
*   npm ≥ 9
    
*   VS Code (recommended)
    

* * *

## Step 1: Project scaffold

```bash
mkdir express-ts-app
cd express-ts-app
npm init -y
```

`npm init -y` creates `package.json` with defaults. You'll update the `scripts` field shortly.

* * *

## Step 2: Install TypeScript

```bash
npm install typescript --save-dev
npx tsc --init
```

`tsc --init` generates `tsconfig.json` with ~100 commented-out options. Replace it with this minimal, strict config:

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "rootDir": "./src",
    "outDir": "./dist",
    "esModuleInterop": true,
    "strict": true
  }
}
```

**Why these settings matter:**

| Option | Why it's here |
| --- | --- |
| `rootDir: ./src` | tsc only compiles files inside `src/` — keeps accidental files out |
| `outDir: ./dist` | compiled JS lands in `dist/`, separate from source |
| `esModuleInterop: true` | enables `import express from 'express'` syntax (vs `require()`) |
| `strict: true` | enables all strict type checks — catches the bugs TypeScript exists to catch |

Forgetting `esModuleInterop` causes a confusing error: `Module '"express"' has no default export`.

* * *

## Step 3: Install Express and its types

```bash
npm install express
npm install @types/express --save-dev
```

Express is written in JavaScript and ships no types. `@types/express` is a separate package that provides TypeScript type definitions for it. Without this, `Request` and `Response` are untyped — you lose all autocomplete and type safety.

* * *

## Step 4: Install ts-node-dev for development

```bash
npm install ts-node-dev --save-dev
```

`ts-node-dev` runs TypeScript files directly without a compile step, and restarts the server whenever you save a file. It's the development equivalent of `nodemon` for plain JS.

* * *

## Step 5: Add npm scripts

Open `package.json` and update the `scripts` field:

```json
"scripts": {
  "dev":   "ts-node-dev --respawn --transpile-only src/app.ts",
  "build": "tsc",
  "start": "node dist/app.js"
}
```

*   `--respawn`: restart the process if it crashes (not just on file change)
    
*   `--transpile-only`: skips full type-checking during development for faster restarts. Type errors still surface in your editor; this just avoids blocking the server restart on them.
    

* * *

## Step 6: Write the Express app

Create `src/app.ts`:

```bash
mkdir src
touch src/app.ts
```

```typescript
import express, { Request, Response } from "express";

const app = express();
const PORT = 3000;

app.use(express.json());

app.get("/health", (req: Request, res: Response) => {
  res.status(200).json({
    status: "OK",
    uptime: process.uptime(),
  });
});

app.get("/", (req: Request, res: Response) => {
  res.send("Express + TypeScript is running");
});

app.listen(PORT, () => {
  console.log(`Server listening on http://localhost:${PORT}`);
});
```

`Request` and `Response` are imported from `express` — not from a separate `@types` import. That's `@types/express` doing its job.

* * *

## Step 7: Run it

**Development** (hot-reload, no compile step):

```bash
npm run dev
```

Hit `http://localhost:3000/health`. Expected response:

```json
{
  "status": "OK",
  "uptime": 2.471
}
```

**Production** (compile first, then run compiled JS):

```bash
npm run build   # tsc → emits dist/app.js
npm start       # node dist/app.js
```

* * *

## Results

After following this guide you have:

*   TypeScript type-checking across your entire Express app
    
*   Hot-reload in development via `ts-node-dev`
    
*   A clean `src/` → `dist/` compilation pipeline for production
    
*   Full autocomplete for `req`, `res`, and Express middleware
    

* * *

## Trade-offs

**Compilation overhead**: `npm run build` adds a compile step before deployment. For small projects, this is negligible (~1–2s). For large monorepos, you'll eventually want incremental compilation (`tsc --incremental`) or a faster bundler like `esbuild`.

`--transpile-only` **skips type-checking at runtime**: Type errors won't block the dev server from starting. Run `npx tsc --noEmit` in CI to catch them before they ship.

`@types/*` **packages lag behind**: Occasionally a major Express update ships before `@types/express` catches up. Check [npmjs.com/package/@types/express](https://www.npmjs.com/package/@types/express) if you hit missing type errors after an upgrade.

* * *

## Conclusion

The core idea: TypeScript lives in `src/`, compiled JavaScript lives in `dist/`. Development uses `ts-node-dev` to skip the compile step. Production uses `tsc` to emit `dist/`, which Node runs directly.

Natural next steps: add an MVC folder structure (`controllers/`, `routes/`, `middleware/`), connect a database (Prisma for PostgreSQL, Mongoose for MongoDB), or add JWT authentication.

* * *

## Further reading

*   [TypeScript Handbook — Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html)
    
*   [Express.js docs](https://expressjs.com/en/4x/api.html)
    
*   [`ts-node-dev` options](https://github.com/wclr/ts-node-dev)
    
*   [tsconfig reference](https://www.typescriptlang.org/tsconfig)
    
*   [DefinitelyTyped — how `@types` packages work](https://definitelytyped.org/)
