TypeScript with Node.js — Full Setup
Advertisement
Setting up TypeScript in Node.js requires more than just installing typescript. You need proper configuration, tooling, and build optimization. This guide walks you through a production-ready setup.
Project Initialization
# Create project directory
mkdir my-api
cd my-api
# Initialize Node.js project
npm init -y
# Install TypeScript and dependencies
npm install --save-dev typescript ts-node @types/node tsx
# Initialize TypeScript config
npx tsc --init
Configure tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Project Structure
my-api/
├── src/
│ ├── index.ts
│ ├── config/
│ │ └── database.ts
│ ├── models/
│ │ └── user.ts
│ ├── routes/
│ │ └── users.ts
│ ├── middleware/
│ │ └── errorHandler.ts
│ └── utils/
│ └── logger.ts
├── dist/
├── tests/
├── .env
├── package.json
├── tsconfig.json
└── README.md
- Project Initialization
- Configure tsconfig.json
- Project Structure
- Setup package.json Scripts
- Basic Express Setup
- Type-Safe Configuration
- Type-Safe Request Handlers
- Environment Variables
- Development Workflow
- Debugging
- Production Build
- Docker Setup
- Common Issues and Solutions
- FAQ
Setup package.json Scripts
{
"name": "my-api",
"version": "1.0.0",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"lint": "eslint src --ext .ts",
"type-check": "tsc --noEmit",
"test": "vitest",
"test:coverage": "vitest --coverage"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"ts-node": "^10.0.0",
"tsx": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"vitest": "^0.34.0"
}
}
Basic Express Setup
// src/index.ts
import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";
dotenv.config();
const app: Express = express();
const port = process.env.PORT || 3000;
// Middleware
app.use(express.json());
// Routes
app.get("/health", (req: Request, res: Response) => {
res.json({ status: "OK" });
});
// Error handling
app.use((err: Error, req: Request, res: Response) => {
console.error(err);
res.status(500).json({ error: err.message });
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
Type-Safe Configuration
// src/config/index.ts
interface Config {
port: number;
nodeEnv: "development" | "production" | "test";
database: {
url: string;
};
jwt: {
secret: string;
expiresIn: string;
};
}
const getConfig = (): Config => {
const nodeEnv = process.env.NODE_ENV as Config["nodeEnv"];
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is required");
}
if (!process.env.JWT_SECRET) {
throw new Error("JWT_SECRET is required");
}
return {
port: parseInt(process.env.PORT || "3000", 10),
nodeEnv: nodeEnv || "development",
database: {
url: process.env.DATABASE_URL,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || "24h",
},
};
};
export const config = getConfig();
Type-Safe Request Handlers
// src/routes/users.ts
import { Router, Request, Response, NextFunction } from "express";
interface User {
id: number;
name: string;
email: string;
}
interface UserRequest extends Request {
user?: User;
}
type Handler = (
req: UserRequest,
res: Response,
next: NextFunction
) => Promise<void> | void;
const asyncHandler =
(fn: Handler) => (req: UserRequest, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
const router = Router();
router.get(
"/:id",
asyncHandler(async (req: UserRequest, res: Response) => {
const { id } = req.params;
// Fetch user
const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
res.json(user);
})
);
export default router;
Environment Variables
# .env
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=super_secret_key_change_in_production
// src/utils/env.ts
const getEnv = (key: string, defaultValue?: string): string => {
const value = process.env[key];
if (value === undefined) {
if (defaultValue !== undefined) {
return defaultValue;
}
throw new Error(`Environment variable ${key} is not defined`);
}
return value;
};
export const env = {
port: parseInt(getEnv("PORT", "3000"), 10),
nodeEnv: getEnv("NODE_ENV", "development"),
databaseUrl: getEnv("DATABASE_URL"),
jwtSecret: getEnv("JWT_SECRET"),
};
Development Workflow
# Watch mode with tsx
npm run dev
# Type checking
npm run type-check
# Build for production
npm run build
# Run production build
npm start
# Run tests
npm test
Debugging
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug TypeScript",
"program": "${workspaceFolder}/src/index.ts",
"preLaunchTask": "tsc: build",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"sourceMap": true,
"cwd": "${workspaceFolder}"
}
]
}
Production Build
# Clean and build
rm -rf dist && npm run build
# Package for deployment
npm prune --production
tar -czf app.tar.gz dist node_modules package.json package-lock.json
Docker Setup
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Build image
npm run build
docker build -t my-api .
# Run container
docker run -p 3000:3000 --env-file .env my-api
Common Issues and Solutions
Problem: Cannot find module errors
// Ensure export statements are present
export { userRouter };
export default app;
Problem: __dirname is not defined
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
FAQ
Q: Should I use tsx or ts-node? A: tsx is faster and more reliable. Use tsx for development. ts-node is good for scripting but slower.
Q: How do I handle async/await without try-catch boilerplate? A: Use wrapper functions like asyncHandler above to automatically catch errors and pass to Express error handlers.
Q: What's the best way to organize a large TypeScript Node.js project? A: Follow feature-based structure: src/features/{feature}/routes.ts, services.ts, types.ts. This scales better than layer-based structure.
A proper TypeScript + Node.js setup takes 30 minutes but saves hours in debugging and maintenance. Start with this foundation and customize as your project grows.
Advertisement