Back to all articles
Featured image for article: Node.js Microservices with Express and TypeScript
Node.js
20 min read1,351 views

Node.js Microservices with Express and TypeScript

Build scalable microservices using Node.js, Express, and TypeScript with proper error handling and monitoring.

#Node.js#TypeScript#Express#Microservices#Backend

Node.js Microservices with Express and TypeScript

Introduction

Node.js is an excellent choice for building microservices due to its non-blocking I/O model and large ecosystem. Combined with TypeScript and Express, you can build robust, type-safe microservices.

Project Structure

src/
  ├── api/
  │   ├── middleware/
  │   ├── routes/
  │   └── validation/
  ├── config/
  ├── services/
  ├── models/
  ├── utils/
  ├── types/
  └── app.ts

Express with TypeScript Setup

typescript
1// app.ts 2import express, { Application, Request, Response, NextFunction } from 'express'; 3import helmet from 'helmet'; 4import cors from 'cors'; 5import compression from 'compression'; 6import rateLimit from 'express-rate-limit'; 7import { errorHandler } from './api/middleware/errorHandler'; 8import { logger } from './utils/logger'; 9 10const app: Application = express(); 11 12// Security middleware 13app.use(helmet()); 14app.use(cors({ 15 origin: process.env.CORS_ORIGIN || '*', 16 credentials: true 17})); 18app.use(compression()); 19 20// Rate limiting 21const limiter = rateLimit({ 22 windowMs: 15 * 60 * 1000, // 15 minutes 23 max: 100, // limit each IP to 100 requests per windowMs 24 message: 'Too many requests from this IP, please try again later.' 25}); 26app.use('/api/', limiter); 27 28// Body parsing 29app.use(express.json({ limit: '10mb' })); 30app.use(express.urlencoded({ extended: true })); 31 32// Request logging 33app.use((req: Request, res: Response, next: NextFunction) => { 34 logger.info(`${req.method} ${req.path}`); 35 next(); 36}); 37 38// Routes 39import userRoutes from './api/routes/userRoutes'; 40import orderRoutes from './api/routes/orderRoutes'; 41app.use('/api/users', userRoutes); 42app.use('/api/orders', orderRoutes); 43 44// Health check 45app.get('/health', (req: Request, res: Response) => { 46 res.json({ status: 'healthy', timestamp: new Date().toISOString() }); 47}); 48 49// 404 handler 50app.use((req: Request, res: Response) => { 51 res.status(404).json({ error: 'Route not found' }); 52}); 53 54// Error handler 55app.use(errorHandler); 56 57const PORT = process.env.PORT || 3000; 58app.listen(PORT, () => { 59 logger.info(`Server running on port ${PORT}`); 60});

Type-Safe Route Handler

typescript
1// api/routes/userRoutes.ts 2import { Router, Request, Response } from 'express'; 3import { body, validationResult } from 'express-validator'; 4import { UserService } from '../../services/UserService'; 5import { asyncHandler } from '../middleware/asyncHandler'; 6 7type CreateUserBody = { 8 email: string; 9 name: string; 10 password: string; 11}; 12 13type UpdateUserBody = Partial<CreateUserBody>; 14 15type UserParams = { 16 id: string; 17}; 18 19const router = Router(); 20const userService = new UserService(); 21 22router.post('/', 23 [ 24 body('email').isEmail().normalizeEmail(), 25 body('name').trim().isLength({ min: 2, max: 50 }), 26 body('password').isLength({ min: 8 }) 27 ], 28 asyncHandler(async (req: Request<{}, {}, CreateUserBody>, res: Response) => { 29 const errors = validationResult(req); 30 if (!errors.isEmpty()) { 31 return res.status(400).json({ errors: errors.array() }); 32 } 33 34 const user = await userService.createUser(req.body); 35 res.status(201).json(user); 36 }) 37); 38 39router.get('/:id', 40 asyncHandler(async (req: Request<UserParams>, res: Response) => { 41 const user = await userService.getUserById(req.params.id); 42 43 if (!user) { 44 return res.status(404).json({ error: 'User not found' }); 45 } 46 47 res.json(user); 48 }) 49); 50 51router.put('/:id', 52 [ 53 body('email').optional().isEmail().normalizeEmail(), 54 body('name').optional().trim().isLength({ min: 2, max: 50 }), 55 body('password').optional().isLength({ min: 8 }) 56 ], 57 asyncHandler(async (req: Request<UserParams, {}, UpdateUserBody>, res: Response) => { 58 const errors = validationResult(req); 59 if (!errors.isEmpty()) { 60 return res.status(400).json({ errors: errors.array() }); 61 } 62 63 const updatedUser = await userService.updateUser(req.params.id, req.body); 64 res.json(updatedUser); 65 }) 66); 67 68export default router;

Service Layer with Dependency Injection

typescript
1// services/UserService.ts 2import { inject, injectable } from 'tsyringe'; 3import { IUserRepository } from '../repositories/IUserRepository'; 4import { ILogger } from '../utils/ILogger'; 5import { User } from '../models/User'; 6 7type CreateUserDTO = { 8 email: string; 9 name: string; 10 password: string; 11}; 12 13@injectable() 14export class UserService { 15 constructor( 16 @inject('IUserRepository') private userRepository: IUserRepository, 17 @inject('ILogger') private logger: ILogger 18 ) {} 19 20 async createUser(data: CreateUserDTO): Promise<User> { 21 this.logger.info('Creating user', { email: data.email }); 22 23 // Check if user exists 24 const existingUser = await this.userRepository.findByEmail(data.email); 25 if (existingUser) { 26 throw new Error('User with this email already exists'); 27 } 28 29 // Hash password 30 const hashedPassword = await this.hashPassword(data.password); 31 32 // Create user 33 const user = await this.userRepository.create({ 34 ...data, 35 password: hashedPassword 36 }); 37 38 this.logger.info('User created successfully', { userId: user.id }); 39 return user; 40 } 41 42 async getUserById(id: string): Promise<User | null> { 43 return this.userRepository.findById(id); 44 } 45 46 async updateUser(id: string, data: Partial<CreateUserDTO>): Promise<User> { 47 const user = await this.userRepository.findById(id); 48 if (!user) { 49 throw new Error('User not found'); 50 } 51 52 if (data.password) { 53 data.password = await this.hashPassword(data.password); 54 } 55 56 const updatedUser = await this.userRepository.update(id, data); 57 return updatedUser; 58 } 59 60 private async hashPassword(password: string): Promise<string> { 61 const bcrypt = await import('bcrypt'); 62 const saltRounds = 10; 63 return bcrypt.hash(password, saltRounds); 64 } 65}

Repository Pattern with TypeORM

typescript
1// repositories/UserRepository.ts 2import { Repository } from 'typeorm'; 3import { AppDataSource } from '../../config/database'; 4import { User } from '../models/User'; 5import { IUserRepository } from './IUserRepository'; 6 7export class UserRepository implements IUserRepository { 8 private repository: Repository<User>; 9 10 constructor() { 11 this.repository = AppDataSource.getRepository(User); 12 } 13 14 async findById(id: string): Promise<User | null> { 15 return this.repository.findOne({ where: { id } }); 16 } 17 18 async findByEmail(email: string): Promise<User | null> { 19 return this.repository.findOne({ where: { email } }); 20 } 21 22 async create(data: Partial<User>): Promise<User> { 23 const user = this.repository.create(data); 24 return this.repository.save(user); 25 } 26 27 async update(id: string, data: Partial<User>): Promise<User> { 28 await this.repository.update(id, data); 29 const user = await this.findById(id); 30 if (!user) { 31 throw new Error('User not found after update'); 32 } 33 return user; 34 } 35 36 async delete(id: string): Promise<boolean> { 37 const result = await this.repository.delete(id); 38 return result.affected !== null && result.affected > 0; 39 } 40}

Error Handling Middleware

typescript
1// api/middleware/errorHandler.ts 2import { Request, Response, NextFunction } from 'express'; 3import { logger } from '../../utils/logger'; 4 5export class AppError extends Error { 6 constructor( 7 public statusCode: number, 8 public message: string, 9 public isOperational = true 10 ) { 11 super(message); 12 Object.setPrototypeOf(this, AppError.prototype); 13 } 14} 15 16export const errorHandler = ( 17 err: Error, 18 req: Request, 19 res: Response, 20 next: NextFunction 21) => { 22 logger.error('Error occurred:', { 23 error: err.message, 24 stack: err.stack, 25 path: req.path, 26 method: req.method 27 }); 28 29 if (err instanceof AppError) { 30 return res.status(err.statusCode).json({ 31 error: err.message, 32 ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) 33 }); 34 } 35 36 // Default error 37 res.status(500).json({ 38 error: 'Internal server error', 39 ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) 40 }); 41};

Testing with Jest

typescript
1// tests/services/UserService.test.ts 2import { UserService } from '../../services/UserService'; 3import { mockUserRepository } from '../mocks/UserRepository'; 4import { mockLogger } from '../mocks/Logger'; 5 6describe('UserService', () => { 7 let userService: UserService; 8 9 beforeEach(() => { 10 userService = new UserService(mockUserRepository, mockLogger); 11 }); 12 13 describe('createUser', () => { 14 it('should create a user successfully', async () => { 15 const userData = { 16 email: 'test@example.com', 17 name: 'Test User', 18 password: 'password123' 19 }; 20 21 mockUserRepository.findByEmail.mockResolvedValue(null); 22 mockUserRepository.create.mockResolvedValue({ 23 id: '1', 24 ...userData, 25 createdAt: new Date() 26 }); 27 28 const result = await userService.createUser(userData); 29 30 expect(result).toHaveProperty('id'); 31 expect(result.email).toBe(userData.email); 32 expect(mockUserRepository.create).toHaveBeenCalledWith( 33 expect.objectContaining({ 34 email: userData.email, 35 name: userData.name 36 }) 37 ); 38 }); 39 40 it('should throw error if user already exists', async () => { 41 const userData = { 42 email: 'existing@example.com', 43 name: 'Existing User', 44 password: 'password123' 45 }; 46 47 mockUserRepository.findByEmail.mockResolvedValue({ 48 id: '1', 49 ...userData 50 }); 51 52 await expect(userService.createUser(userData)) 53 .rejects 54 .toThrow('User with this email already exists'); 55 }); 56 }); 57});

Docker Configuration

dockerfile
1# Dockerfile 2FROM node:18-alpine 3 4WORKDIR /app 5 6# Install dependencies 7COPY package*.json ./ 8RUN npm ci --only=production 9 10# Copy source 11COPY . . 12 13# Build TypeScript 14RUN npm run build 15 16# Remove dev dependencies 17RUN npm prune --production 18 19EXPOSE 3000 20 21CMD ["node", "dist/app.js"]
yaml
1# docker-compose.yml 2version: '3.8' 3services: 4 api: 5 build: . 6 ports: 7 - "3000:3000" 8 environment: 9 - NODE_ENV=production 10 - DATABASE_URL=postgresql://db:5432/mydb 11 - REDIS_URL=redis://redis:6379 12 depends_on: 13 - db 14 - redis 15 restart: unless-stopped 16 17 db: 18 image: postgres:15-alpine 19 environment: 20 - POSTGRES_DB=mydb 21 - POSTGRES_USER=user 22 - POSTGRES_PASSWORD=password 23 volumes: 24 - postgres_data:/var/lib/postgresql/data 25 26 redis: 27 image: redis:7-alpine 28 command: redis-server --appendonly yes 29 volumes: 30 - redis_data:/data 31 32volumes: 33 postgres_data: 34 redis_data:
Profile picture of Sumit Kumar Pandey

Sumit Kumar Pandey

Full-Stack Developer

Full-Stack Developer with 5+ years of experience building scalable web applications. Passionate about clean code, performance optimization, and modern web technologies.

About the Author

Author information for Sumit Kumar Pandey

Share this article

Found this helpful? Share with your network!

0 shares

Discussion (0)

Share your thoughts and join the conversation

Leave a comment

Be respectful and stay on topic

Write your comment in the text area above. Comments should be respectful and relevant to the article.

AI Chat Assistant

Interactive AI assistant for Sumit Kumar Pandey's portfolio website. Ask questions about technical skills, work experience, projects, availability, and contact information. Powered by Next.js API.