If you've been writing JavaScript for a while, you've probably heard people talk about TypeScript. It adds static types to JavaScript, which means you catch bugs before running your code, get better autocomplete in your editor, and write more self-documenting code. In this post, we'll set up a clean Node.js + Express backend using TypeScript from scratch, the same setup I use as a starting point for my own projects.
By the end, you'll have:
- An Express server running TypeScript
- Hot-reload in development (save a file and server restarts automatically)
- A clean separation between dev and production workflows
Prerequisites: Node.js installed, basic JavaScript knowledge. That's it.
Step 1 — Initialize the Project
Create a new folder for your project and initialize a package.json:
mkdir my-backend && cd my-backend
pnpm init
Don't have pnpm? Install it with npm install -g pnpm, or just replace pnpm with npm throughout this post.
Step 2 — Install Dependencies
Install the production dependencies:
pnpm add express dotenv
-
express— the web framework we'll use to handle HTTP requests -
dotenv— loads environment variables from a .env file (so you can store things like your port number or API keys outside your code)
Now install the development dependencies:
pnpm add -D typescript ts-node nodemon @types/node @types/express
These are only needed while building the project, not when running it in production:
-
typescript— the TypeScript compiler that turns .ts files into plain JavaScript -
ts-node— lets you run .ts files directly without compiling first (great for development) -
nodemon— watches your files and automatically restarts the server when you save a change -
@types/nodeand@types/express— type definitions that teach TypeScript what Node.js and Express APIs look like
Step 3 — Configure TypeScript
Create a tsconfig.json file at the root of your project:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}
Here's what the key options mean:
target: "ES2020" - Compile to modern JavaScript (Node.js 14+ supports this)
rootDir: "src" - TypeScript source files live in the src/ folder
outDir: "dist" - Compiled JavaScript output goes into dist/
strict: true - Turns on strict type checking — catches more bugs
esModuleInterop: true - Lets you use import x from 'y' syntax with CommonJS packages
skipLibCheck: true - Skips type checking inside node_modules — avoids noisy errors
Step 4 — Configure Nodemon
Create a nodemon.json file at the root:
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["dist"],
"exec": "pnpm exec ts-node src/index.ts"
}
- watch: ["src"] — only watch the src/ folder for changes
- ext: "ts,json" — restart when .ts or .json files change
- ignore: ["dist"] — don't watch the compiled output folder (this would cause an infinite restart loop)
- exec — the command to run when a change is detected; we use pnpm exec ts-node to run TypeScript directly
Step 5 — Add Scripts to package.json
Update your package.json scripts section:
"scripts": {
"dev": "nodemon",
"build": "tsc",
"start": "node dist/index.js"
}
- dev — starts nodemon, which runs your TypeScript directly with hot-reload
- build — compiles your TypeScript to JavaScript in dist/
- start — runs the compiled JavaScript (for production)
Step 6 — Set Up Environment Variables
Create a .env file at the root:
PORT=4000
Then create a .gitignore file (you don't want to commit your secrets or compiled files):
node_modules/
dist/
.env
Step 7 — Write the Server
Create a src/ folder, then create src/index.ts:
`import application from "express";
import { configDotenv } from "dotenv";
configDotenv();
const PORT = process.env.PORT || 3000;
const app = application();
app.use(application.json());
app.get("/", (_req, res) => {
res.json({ message: "Server is running" });
});
app.listen(PORT, () => {
console.log(Server is running at Port ${PORT});
});`
A few things to note:
import application from "express" — we're importing Express and calling it application. The name after import is just a variable name — you can call it anything you like (express, app, server). The important part is what comes after from.
configDotenv() — loads your .env file. This must run before you read process.env.PORT, otherwise the variable won't be set yet.
application.json() — middleware that parses incoming JSON request bodies. You'll need this for any POST/PUT endpoints that receive JSON data.
app.get("/") — a simple health-check route. When you visit http://localhost:4000/ in your browser, you'll see {"message":"Server is running"}. This is how you'll verify everything is working.
Step 8 — Run It
Start the development server:
pnpm dev
You should see:
[nodemon] 3.1.14pnpm exec ts-node src/index.ts
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: ts,json
[nodemon] starting
Server is running at Port 4000
Open your browser and visit http://localhost:4000/ — you should see:
{ "message": "Server is running" }
When you're ready to build for production:
pnpm build - compiles TypeScript → dist/
pnpm start - runs the compiled JavaScript
What's Next
You now have a solid foundation: TypeScript, Express, hot-reload in dev, and a clean build pipeline for production. From here you can add routes, connect a database, or add authentication middleware.
Top comments (0)