Building Scalable REST APIs with Node.js and TypeScript
BackendDecember 10, 202515 min read0 views
Node.jsTypeScriptExpressREST APIBackendBest Practices
Share:
Building Scalable REST APIs with Node.js and TypeScript
Learn how to build production-ready REST APIs using modern best practices with Node.js, Express, and TypeScript.
Project Setup
Let's start by setting up a solid TypeScript project structure.
mkdir api-server && cd api-server npm init -y npm install express cors helmet morgan npm install -D typescript @types/node @types/express ts-node-dev
TypeScript Configuration
// tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "commonjs", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }
Project Structure
src/
├── config/
│ └── database.ts
├── controllers/
│ └── user.controller.ts
├── middleware/
│ ├── auth.middleware.ts
│ ├── error.middleware.ts
│ └── validate.middleware.ts
├── models/
│ └── user.model.ts
├── routes/
│ └── user.routes.ts
├── services/
│ └── user.service.ts
├── types/
│ └── express.d.ts
├── utils/
│ ├── ApiError.ts
│ └── catchAsync.ts
└── app.ts
Setting Up Express
// src/app.ts import express, { Application } from 'express'; import cors from 'cors'; import helmet from 'helmet'; import morgan from 'morgan'; import userRoutes from './routes/user.routes'; import { errorHandler } from './middleware/error.middleware'; const app: Application = express(); // Security middleware app.use(helmet()); app.use(cors()); // Logging app.use(morgan('dev')); // Body parsing app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Routes app.use('/api/v1/users', userRoutes); // Error handling app.use(errorHandler); export default app;
Custom Error Handling
// src/utils/ApiError.ts export class ApiError extends Error { statusCode: number; isOperational: boolean; constructor(statusCode: number, message: string, isOperational = true) { super(message); this.statusCode = statusCode; this.isOperational = isOperational; Error.captureStackTrace(this, this.constructor); } } // src/middleware/error.middleware.ts import { Request, Response, NextFunction } from 'express'; import { ApiError } from '../utils/ApiError'; export const errorHandler = ( err: Error, req: Request, res: Response, next: NextFunction ) => { if (err instanceof ApiError) { return res.status(err.statusCode).json({ success: false, message: err.message, }); } // Unhandled errors console.error(err); return res.status(500).json({ success: false, message: 'Internal server error', }); };
Async Error Handling
// src/utils/catchAsync.ts import { Request, Response, NextFunction } from 'express'; export const catchAsync = (fn: Function) => { return (req: Request, res: Response, next: NextFunction) => { Promise.resolve(fn(req, res, next)).catch(next); }; };
Request Validation
// src/middleware/validate.middleware.ts import { Request, Response, NextFunction } from 'express'; import Joi from 'joi'; import { ApiError } from '../utils/ApiError'; export const validate = (schema: Joi.ObjectSchema) => { return (req: Request, res: Response, next: NextFunction) => { const { error, value } = schema.validate(req.body, { abortEarly: false, stripUnknown: true, }); if (error) { const message = error.details.map((d) => d.message).join(', '); return next(new ApiError(400, message)); } req.body = value; next(); }; };
Controller Pattern
// src/controllers/user.controller.ts import { Request, Response } from 'express'; import { catchAsync } from '../utils/catchAsync'; import { UserService } from '../services/user.service'; import { ApiError } from '../utils/ApiError'; const userService = new UserService(); export const getUsers = catchAsync(async (req: Request, res: Response) => { const users = await userService.getAllUsers(); res.status(200).json({ success: true, data: users, }); }); export const createUser = catchAsync(async (req: Request, res: Response) => { const user = await userService.createUser(req.body); res.status(201).json({ success: true, data: user, }); }); export const getUserById = catchAsync(async (req: Request, res: Response) => { const user = await userService.getUserById(req.params.id); if (!user) { throw new ApiError(404, 'User not found'); } res.status(200).json({ success: true, data: user, }); });
Service Layer
// src/services/user.service.ts import { User } from '../models/user.model'; import { ApiError } from '../utils/ApiError'; export class UserService { async getAllUsers() { return await User.find(); } async getUserById(id: string) { const user = await User.findById(id); return user; } async createUser(userData: any) { const existingUser = await User.findOne({ email: userData.email }); if (existingUser) { throw new ApiError(400, 'Email already in use'); } const user = await User.create(userData); return user; } async updateUser(id: string, updateData: any) { const user = await User.findByIdAndUpdate(id, updateData, { new: true }); if (!user) { throw new ApiError(404, 'User not found'); } return user; } async deleteUser(id: string) { const user = await User.findByIdAndDelete(id); if (!user) { throw new ApiError(404, 'User not found'); } return user; } }
Routes
// src/routes/user.routes.ts import { Router } from 'express'; import * as userController from '../controllers/user.controller'; import { validate } from '../middleware/validate.middleware'; import { createUserSchema, updateUserSchema } from '../validations/user.validation'; import { auth } from '../middleware/auth.middleware'; const router = Router(); router .route('/') .get(auth, userController.getUsers) .post(validate(createUserSchema), userController.createUser); router .route('/:id') .get(auth, userController.getUserById) .patch(auth, validate(updateUserSchema), userController.updateUser) .delete(auth, userController.deleteUser); export default router;
Best Practices Summary
- ✅ Use TypeScript for type safety
- ✅ Layered architecture (routes → controllers → services → models)
- ✅ Centralized error handling
- ✅ Request validation with Joi or Zod
- ✅ Security middleware (helmet, cors, rate limiting)
- ✅ Proper logging with Morgan or Winston
- ✅ Environment variables with dotenv
- ✅ API versioning (/api/v1/)
- ✅ Consistent response format
- ✅ Authentication & authorization
Conclusion
This architecture provides a solid foundation for building scalable REST APIs. It's maintainable, testable, and follows industry best practices.
Start building your next API with confidence! 🚀