Node.js with TypeScript
TypeScript brings static typing and modern language features to Node.js development, enabling more maintainable server-side applications. With TypeScript, you can catch errors at compile time, enjoy better IDE support, and write more self-documenting code for your Node.js projects.
TypeScript's integration with Node.js provides excellent tooling for building APIs, web servers, CLI tools, and microservices with enhanced developer experience and runtime reliability.
Project Setup and Configuration
Basic Node.js TypeScript Setup
# Initialize project
npm init -y
# Install TypeScript dependencies
npm install -D typescript @types/node ts-node nodemon
npm install express
# Install type definitions for Express
npm install -D @types/express
# Create TypeScript config
npx tsc --init
Package.json Scripts
{
"name": "nodejs-typescript-app",
"version": "1.0.0",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "nodemon src/index.ts",
"test": "jest",
"lint": "eslint src/**/*.ts"
},
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"@types/express": "^4.17.0",
"@types/node": "^18.0.0",
"nodemon": "^2.0.0",
"ts-node": "^10.0.0",
"typescript": "^4.8.0"
}
}
TypeScript Configuration for Node.js
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"lib": ["es2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModules": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"@controllers/*": ["src/controllers/*"],
"@services/*": ["src/services/*"],
"@models/*": ["src/models/*"],
"@utils/*": ["src/utils/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Express.js with TypeScript
Basic Express Server
// src/index.ts
import express, { Express, Request, Response } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
dotenv.config();
const app: Express = express();
const port = process.env.PORT || 3000;
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Basic route
app.get('/', (req: Request, res: Response) => {
res.json({ message: 'Hello TypeScript + Node.js!' });
});
// Health check
app.get('/health', (req: Request, res: Response) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
app.listen(port, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
});
export default app;
Typed Request and Response
// src/types/express.ts
import { Request, Response } from 'express';
// Extend Express Request type
export interface AuthenticatedRequest extends Request {
user?: {
id: string;
email: string;
role: string;
};
}
// Custom response interfaces
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
errors?: string[];
}
// Typed route handlers
export type RouteHandler<T = any> = (
req: Request,
res: Response<ApiResponse<T>>
) => Promise<void> | void;
export type AuthenticatedRouteHandler<T = any> = (
req: AuthenticatedRequest,
res: Response<ApiResponse<T>>
) => Promise<void> | void;
// Usage example
const getUserProfile: AuthenticatedRouteHandler<User> = async (req, res) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({
success: false,
message: 'User not authenticated'
});
}
const user = await getUserById(userId);
res.json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
};
Controllers and Route Organization
// src/models/User.ts
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user';
createdAt: Date;
updatedAt: Date;
}
export interface CreateUserDto {
email: string;
name: string;
password: string;
}
export interface UpdateUserDto {
name?: string;
email?: string;
}
// src/controllers/UserController.ts
import { Request, Response } from 'express';
import { UserService } from '@services/UserService';
import { CreateUserDto, UpdateUserDto } from '@models/User';
import { ApiResponse, AuthenticatedRequest } from '@/types/express';
export class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
getAllUsers = async (req: Request, res: Response<ApiResponse<User[]>>) => {
try {
const { page = 1, limit = 10, search } = req.query;
const users = await this.userService.getAllUsers({
page: Number(page),
limit: Number(limit),
search: search as string
});
res.json({
success: true,
data: users
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to fetch users'
});
}
};
getUserById = async (req: Request, res: Response<ApiResponse<User>>) => {
try {
const { id } = req.params;
const user = await this.userService.getUserById(id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to fetch user'
});
}
};
createUser = async (req: Request<{}, ApiResponse<User>, CreateUserDto>, res: Response<ApiResponse<User>>) => {
try {
const userData = req.body;
const newUser = await this.userService.createUser(userData);
res.status(201).json({
success: true,
data: newUser
});
} catch (error) {
res.status(400).json({
success: false,
message: 'Failed to create user'
});
}
};
updateUser = async (req: Request<{ id: string }, ApiResponse<User>, UpdateUserDto>, res: Response<ApiResponse<User>>) => {
try {
const { id } = req.params;
const updates = req.body;
const updatedUser = await this.userService.updateUser(id, updates);
res.json({
success: true,
data: updatedUser
});
} catch (error) {
res.status(400).json({
success: false,
message: 'Failed to update user'
});
}
};
deleteUser = async (req: Request, res: Response<ApiResponse>) => {
try {
const { id } = req.params;
await this.userService.deleteUser(id);
res.json({
success: true,
message: 'User deleted successfully'
});
} catch (error) {
res.status(400).json({
success: false,
message: 'Failed to delete user'
});
}
};
}
Middleware with TypeScript
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { AuthenticatedRequest } from '@/types/express';
interface JwtPayload {
userId: string;
email: string;
role: string;
}
export const authenticateToken = (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
res.status(401).json({
success: false,
message: 'Access token required'
});
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
req.user = {
id: decoded.userId,
email: decoded.email,
role: decoded.role
};
next();
} catch (error) {
res.status(403).json({
success: false,
message: 'Invalid token'
});
}
};
// Role-based authorization middleware
export const requireRole = (roles: string[]) => {
return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
if (!req.user) {
res.status(401).json({
success: false,
message: 'Authentication required'
});
return;
}
if (!roles.includes(req.user.role)) {
res.status(403).json({
success: false,
message: 'Insufficient permissions'
});
return;
}
next();
};
};
// Validation middleware
export const validateRequest = <T>(validator: (body: any) => T | null) => {
return (req: Request, res: Response, next: NextFunction): void => {
try {
const validatedBody = validator(req.body);
if (!validatedBody) {
res.status(400).json({
success: false,
message: 'Invalid request data'
});
return;
}
req.body = validatedBody;
next();
} catch (error) {
res.status(400).json({
success: false,
message: 'Validation failed'
});
}
};
};
// Error handling middleware
export const errorHandler = (
error: Error,
req: Request,
res: Response,
next: NextFunction
): void => {
console.error('Error:', error);
res.status(500).json({
success: false,
message: process.env.NODE_ENV === 'production'
? 'Internal server error'
: error.message
});
};
Service Layer
// src/services/UserService.ts
import { User, CreateUserDto, UpdateUserDto } from '@models/User';
import bcrypt from 'bcrypt';
export interface PaginationOptions {
page: number;
limit: number;
search?: string;
}
export interface PaginatedResult<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
export class UserService {
private users: User[] = []; // In real app, this would be a database
async getAllUsers(options: PaginationOptions): Promise<PaginatedResult<User>> {
let filteredUsers = this.users;
// Apply search filter
if (options.search) {
const searchLower = options.search.toLowerCase();
filteredUsers = this.users.filter(user =>
user.name.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower)
);
}
// Apply pagination
const startIndex = (options.page - 1) * options.limit;
const endIndex = startIndex + options.limit;
const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
return {
data: paginatedUsers,
pagination: {
page: options.page,
limit: options.limit,
total: filteredUsers.length,
totalPages: Math.ceil(filteredUsers.length / options.limit)
}
};
}
async getUserById(id: string): Promise<User | null> {
return this.users.find(user => user.id === id) || null;
}
async getUserByEmail(email: string): Promise<User | null> {
return this.users.find(user => user.email === email) || null;
}
async createUser(userData: CreateUserDto): Promise<User> {
// Check if user already exists
const existingUser = await this.getUserByEmail(userData.email);
if (existingUser) {
throw new Error('User with this email already exists');
}
// Hash password
const hashedPassword = await bcrypt.hash(userData.password, 10);
const newUser: User = {
id: Math.random().toString(36).substr(2, 9),
email: userData.email,
name: userData.name,
role: 'user',
createdAt: new Date(),
updatedAt: new Date()
};
this.users.push(newUser);
return newUser;
}
async updateUser(id: string, updates: UpdateUserDto): Promise<User> {
const userIndex = this.users.findIndex(user => user.id === id);
if (userIndex === -1) {
throw new Error('User not found');
}
const updatedUser: User = {
...this.users[userIndex],
...updates,
updatedAt: new Date()
};
this.users[userIndex] = updatedUser;
return updatedUser;
}
async deleteUser(id: string): Promise<void> {
const userIndex = this.users.findIndex(user => user.id === id);
if (userIndex === -1) {
throw new Error('User not found');
}
this.users.splice(userIndex, 1);
}
}
Database Integration
// src/database/connection.ts
import { Pool } from 'pg';
import { config } from '@/config/database';
export class Database {
private pool: Pool;
constructor() {
this.pool = new Pool(config);
}
async query<T = any>(text: string, params?: any[]): Promise<T[]> {
const client = await this.pool.connect();
try {
const result = await client.query(text, params);
return result.rows;
} finally {
client.release();
}
}
async queryOne<T = any>(text: string, params?: any[]): Promise<T | null> {
const results = await this.query<T>(text, params);
return results[0] || null;
}
async transaction<T>(callback: (query: Database['query']) => Promise<T>): Promise<T> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
const transactionQuery = async (text: string, params?: any[]) => {
const result = await client.query(text, params);
return result.rows;
};
const result = await callback(transactionQuery);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async close(): Promise<void> {
await this.pool.end();
}
}
// src/repositories/UserRepository.ts
import { Database } from '@/database/connection';
import { User, CreateUserDto, UpdateUserDto } from '@models/User';
export class UserRepository {
private db: Database;
constructor() {
this.db = new Database();
}
async findAll(limit: number, offset: number): Promise<User[]> {
const query = `
SELECT id, email, name, role, created_at, updated_at
FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`;
return this.db.query<User>(query, [limit, offset]);
}
async findById(id: string): Promise<User | null> {
const query = `
SELECT id, email, name, role, created_at, updated_at
FROM users
WHERE id = $1
`;
return this.db.queryOne<User>(query, [id]);
}
async findByEmail(email: string): Promise<User | null> {
const query = `
SELECT id, email, name, role, created_at, updated_at
FROM users
WHERE email = $1
`;
return this.db.queryOne<User>(query, [email]);
}
async create(userData: CreateUserDto & { hashedPassword: string }): Promise<User> {
const query = `
INSERT INTO users (email, name, password, role)
VALUES ($1, $2, $3, 'user')
RETURNING id, email, name, role, created_at, updated_at
`;
const result = await this.db.query<User>(query, [
userData.email,
userData.name,
userData.hashedPassword
]);
return result[0];
}
async update(id: string, updates: UpdateUserDto): Promise<User> {
const setClause = Object.keys(updates)
.map((key, index) => `${key} = $${index + 2}`)
.join(', ');
const query = `
UPDATE users
SET ${setClause}, updated_at = NOW()
WHERE id = $1
RETURNING id, email, name, role, created_at, updated_at
`;
const values = [id, ...Object.values(updates)];
const result = await this.db.query<User>(query, values);
return result[0];
}
async delete(id: string): Promise<void> {
const query = 'DELETE FROM users WHERE id = $1';
await this.db.query(query, [id]);
}
}
Environment Configuration
// src/config/environment.ts
import dotenv from 'dotenv';
dotenv.config();
interface Config {
port: number;
nodeEnv: string;
jwtSecret: string;
database: {
host: string;
port: number;
name: string;
user: string;
password: string;
};
redis: {
host: string;
port: number;
password?: string;
};
}
const config: Config = {
port: parseInt(process.env.PORT || '3000', 10),
nodeEnv: process.env.NODE_ENV || 'development',
jwtSecret: process.env.JWT_SECRET || 'fallback-secret',
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
name: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password'
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
password: process.env.REDIS_PASSWORD
}
};
export default config;
Testing with Jest
// src/__tests__/UserService.test.ts
import { UserService } from '@services/UserService';
import { CreateUserDto } from '@models/User';
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
});
describe('createUser', () => {
it('should create a new user successfully', async () => {
const userData: CreateUserDto = {
email: '[email protected]',
name: 'Test User',
password: 'password123'
};
const user = await userService.createUser(userData);
expect(user).toBeDefined();
expect(user.email).toBe(userData.email);
expect(user.name).toBe(userData.name);
expect(user.role).toBe('user');
expect(user.id).toBeDefined();
});
it('should throw error if user already exists', async () => {
const userData: CreateUserDto = {
email: '[email protected]',
name: 'Test User',
password: 'password123'
};
await userService.createUser(userData);
await expect(userService.createUser(userData)).rejects.toThrow(
'User with this email already exists'
);
});
});
describe('getUserById', () => {
it('should return user if found', async () => {
const userData: CreateUserDto = {
email: '[email protected]',
name: 'Test User',
password: 'password123'
};
const createdUser = await userService.createUser(userData);
const foundUser = await userService.getUserById(createdUser.id);
expect(foundUser).toEqual(createdUser);
});
it('should return null if user not found', async () => {
const user = await userService.getUserById('nonexistent-id');
expect(user).toBeNull();
});
});
});
Node.js with TypeScript provides a robust foundation for building scalable server-side applications. The static typing helps catch errors early, improves code maintainability, and provides excellent developer experience with modern tooling and IDE support.