Skip to main content
Building Scalable REST APIs with Node.js and TypeScript

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

  1. Use TypeScript for type safety
  2. Layered architecture (routes → controllers → services → models)
  3. Centralized error handling
  4. Request validation with Joi or Zod
  5. Security middleware (helmet, cors, rate limiting)
  6. Proper logging with Morgan or Winston
  7. Environment variables with dotenv
  8. API versioning (/api/v1/)
  9. Consistent response format
  10. 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! 🚀

Let's Connect

Ready to build something amazing together?

Send us a message

🚀

Let's Chat.

Tell me about your project.

Let's create something together 🤝

Visit my social profile and get connected