Building a TypeScript REST API with an Object-Oriented Programming (OOP) Approach

Abayomi Ogunnusi - Jun 26 - - Dev Community
Rest Api using TypeScript (OOP approach)

In this tutorial we will create a rest api using TypeScript. We will use OOP approach to create the api.

Why Object-Oriented Programming (OOP) approach?

  • OOP allows you to create reusable code that is easy to maintain and extend.
  • OOP provides a clear and organized structure for your code.
  • OOP promotes code reusability and modularity.
  • OOP makes it easier to manage complex systems by breaking them down into smaller, more manageable pieces.

Prerequisites

  • Node.js
  • TypeScript
  • Express.js
  • MongoDB

Step 1: Check if Node.js is installed

Open your terminal and type the following command:

node -v
Enter fullscreen mode Exit fullscreen mode

If Node.js is installed, you will see the version number. If not, you can download it from here.

Step 2: Check if TypeScript is installed

Open your terminal and type the following command:

tsc -v
Enter fullscreen mode Exit fullscreen mode

If TypeScript is installed, you will see the version number. If not, you can install it by running the following command:

npm install -g typescript
Enter fullscreen mode Exit fullscreen mode

Step 3: Create a Node.js project

Create a new directory for your project and navigate to it:

mkdir oop-rest-api
cd oop-rest-api
Enter fullscreen mode Exit fullscreen mode

Step 4: Initialize the project

Run the following command to initialize the project:

npm init -y
Enter fullscreen mode Exit fullscreen mode

Step 5: Install the required packages

npm install express mongoose cors helmet dotenv
npm install --save-dev typescript @types/node @types/express @types/mongoose @types/cors @types/helmet
Enter fullscreen mode Exit fullscreen mode

Step 6: Create a tsconfig.json file

tsc --init
Enter fullscreen mode Exit fullscreen mode

Step 7: Create a src directory and other required directories

mkdir src src/controllers src/models src/routes src/services src/helpers src/config src/interfaces src/repository
Enter fullscreen mode Exit fullscreen mode

Step 8: Create a user model

Create a new file in the src/models directory called user.model.ts and add the following code:

import mongoose from "mongoose";
import IUser from "../interfaces/IUser";

const userSchema = new mongoose.Schema(
  {
    name: { type: String, required: true },
    email: { type: String, required: true },
    password: { type: String, required: true },
  },
  {
    timestamps: true,
  }
);

const User = mongoose.model<IUser>("User", userSchema);
export default User;
Enter fullscreen mode Exit fullscreen mode

Step 9: Create an interface for the user model

Create a new file in the src/interfaces directory called user.interface.ts and add the following code:

import mongoose from "mongoose";
interface IUser extends mongoose.Document {
  id: number;
  name: string;
  email: string;
  password: string;
}

export default IUser;
Enter fullscreen mode Exit fullscreen mode

Step 10: Create a Base Repository

export interface BaseRepository<T> {
  create(data: T): Promise<T>;
  findAll(): Promise<T[]>;
  findById(id: string): Promise<T | null>;
  update(id: string, data: T): Promise<T | null>;
  delete(id: string): Promise<T | null>;
  findAllPaginatedWithFilter(
    filter: any,
    page: number,
    limit: number
  ): Promise<T[]>;
}
Enter fullscreen mode Exit fullscreen mode

Step 11: Create a Database Connection

Create a new file in the src/config directory called db.ts and add the following code:

import mongoose from "mongoose";

class Database {
  private readonly URI: string;

  constructor() {
    this.URI =
      process.env.MONGO_URI || "mongodb://localhost:27017/express-mongo";
    this.connect();
  }

  private async connect() {
    try {
      await mongoose.connect(this.URI);
      console.log("Database connected successfully");
    } catch (error) {
      console.error("Database connection failed");
    }
  }
}

export default Database;
Enter fullscreen mode Exit fullscreen mode

Step 12: Create a user repository generic class and implement the base repository

Create a new file in the src/repository directory called generic.repository.ts and add the following code:

import { BaseRepository } from "../interfaces/base.repository";
import mongoose from "mongoose";

class GenericRepository<T extends mongoose.Document>
  implements BaseRepository<T>
{
  private readonly model: mongoose.Model<T>;

  constructor(model: mongoose.Model<T>) {
    this.model = model;
  }

  async create(data: T): Promise<T> {
    return this.model.create(data);
  }

  async findAll(): Promise<T[]> {
    return this.model.find().exec();
  }

  async findById(id: string): Promise<T | null> {
    return this.model.findById(id).exec();
  }

  async update(id: string, data: T): Promise<T | null> {
    return this.model.findByIdAndUpdate(id, data, { new: true }).exec();
  }

  async delete(id: string): Promise<T | null> {
    return this.model.findByIdAndDelete(id).exec();
  }

  async findAllPaginatedWithFilter(
    filter: any,
    page: number,
    limit: number
  ): Promise<T[]> {
    return this.model
      .find(filter)
      .skip((page - 1) * limit)
      .limit(limit)
      .exec();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 13: Create a user repository

Create a new file in the src/repository directory called user.repository.ts and add the following code:

import IUser from "../interfaces/IUser";
import User from "../models/user.model";
import GenericRepository from "./generic.repository";

class UserRepository extends GenericRepository<IUser> {
  constructor() {
    super(User);
  }

  // create custom methods for user repository
  async findByEmail(email: string): Promise<IUser | null> {
    return User.findOne({ email });
  }

  async findByName(name: string): Promise<IUser | null> {
    return User.findOne({ name });
  }
}

export default UserRepository;
Enter fullscreen mode Exit fullscreen mode

Step 14: Create a user service

Create a new file in the src/services directory called user.service.ts and add the following code:

import IUser from "../interfaces/IUser";
import UserRepository from "../repository/user.repository";

class UserService {
  private readonly userRepository: UserRepository;

  constructor() {
    this.userRepository = new UserRepository();
  }

  async create(data: IUser): Promise<IUser> {
    return this.userRepository.create(data);
  }

  async findAll(): Promise<IUser[]> {
    return this.userRepository.findAll();
  }

  async findById(id: string): Promise<IUser | null> {
    return this.userRepository.findById(id);
  }

  async update(id: string, data: IUser): Promise<IUser | null> {
    return this.userRepository.update(id, data);
  }

  async delete(id: string): Promise<IUser | null> {
    return this.userRepository.delete(id);
  }

  async findByEmail(email: string): Promise<IUser | null> {
    return this.userRepository.findByEmail(email);
  }

  async findByName(name: string): Promise<IUser | null> {
    return this.userRepository.findByName(name);
  }
}

export default UserService;
Enter fullscreen mode Exit fullscreen mode

Step 15: Create a user controller

Create a new file in the src/controllers directory called user.controller.ts and add the following code:

import { Request, Response } from "express";
import IUser from "../interfaces/IUser";

import UserService from "../services/user.service";

class UserController {
  private readonly userService: UserService;

  constructor() {
    this.userService = new UserService();
  }

  async create(req: Request, res: Response) {
    try {
      const data: IUser = req.body;
      const user = await this.userService.create(data);
      res.status(201).json(user);
    } catch (error: unknown) {
      throw new Error(error as string);
    }
  }

  async findAll(req: Request, res: Response) {
    try {
      const users = await this.userService.findAll();
      res.status(200).json(users);
    } catch (error) {
      throw new Error(error as string);
    }
  }
}

export default UserController;
Enter fullscreen mode Exit fullscreen mode

Step 16: Create a user route

Create a new file in the src/routes directory called user.route.ts and add the following code:

import { Router } from "express";
import UserController from "../controllers/user.controller";

class UserRoute {
  private readonly userController: UserController;
  public readonly router: Router;

  constructor() {
    this.userController = new UserController();
    this.router = Router();
    this.initRoutes();
  }

  private initRoutes() {
    this.router.post("/", this.userController.create.bind(this.userController));
    this.router.get("/", this.userController.findAll.bind(this.userController));
  }
}

export default new UserRoute().router;
Enter fullscreen mode Exit fullscreen mode

Step 17: Create global error handler middleware

Create a new file in the src/helpers directory called error-handler.ts and add the following code:

import { Request, Response, NextFunction } from "express";

// write a single class for 404 and 500 error
import { Request, Response, NextFunction } from "express";

class ErrorHandler {
  static notFound(req: Request, res: Response, next: NextFunction) {
    res.status(404).json({ message: "Resource not found" });
  }

  static serverError(
    error: Error,
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    res.status(500).json({ message: error.message });
  }
}

export default ErrorHandler;
Enter fullscreen mode Exit fullscreen mode

Step 18: Create an App class

Create a new file in the src directory called app.ts and add the following code:

import express, { Application } from "express";
import cors from "cors";
import helmet from "helmet";
import ErrorHandler from "./helpers/error-handler";
import Database from "./config/config";
import dotenv from "dotenv";
import UserRoute from "./routes/user.routes";
import userRoutes from "./routes/user.routes";

class App {
  private readonly app: Application;
  private readonly port: number;

  constructor() {
    this.app = express();
    this.port = parseInt(process.env.PORT || "3000");
    this.init();
  }

  private init() {
    this.initConfig();
    this.initMiddlewares();
    this.initRoutes();
    this.initErrorHandling();
  }

  private initConfig() {
    new Database();
  }

  private initMiddlewares() {
    this.app.use(cors());
    this.app.use(helmet());
    this.app.use(express.json());
    this.app.use(express.urlencoded({ extended: true }));
    dotenv.config();
  }

  private initRoutes() {
    this.app.use("/api/v1/users", userRoutes);
  }

  private initErrorHandling() {
    this.app.use(ErrorHandler.notFound);
    this.app.use(ErrorHandler.serverError);
  }

  public listen() {
    this.app.listen(this.port, () => {
      console.log(`Server is running on http://localhost:${this.port}`);
    });
  }
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Step 19: Create an index file

Create a new file in the src directory called index.ts and add the following code:

import App from "./app";

const app = new App();

app.listen();
Enter fullscreen mode Exit fullscreen mode

Step 20: Write your scripts in package.json

"scripts": {
    "start": "node dist/index.js",
    "dev": "nodemon src/index.ts",
    "build": "tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  }
Enter fullscreen mode Exit fullscreen mode

Step 21: Run the application

npm run dev
Enter fullscreen mode Exit fullscreen mode

Step 22: Test the application

Create a test.http file in the root directory and add the following code:


### 404 Not Found
GET http://localhost:3000/api/v1/userspost
Enter fullscreen mode Exit fullscreen mode

Image description

### Create a new user
POST http://localhost:3000/api/v1/users
Content-Type: application/json

{
  "name": "John Doe",
  "email": "luli@yopmail.com",
  "password": "password"
}
Enter fullscreen mode Exit fullscreen mode

Image description

### Get all users
GET http://localhost:3000/api/v1/users
Enter fullscreen mode Exit fullscreen mode

Image description

Conclusion

We have successfully created a rest api using TypeScript and OOP approach. Feel free to expand on this project by adding more features and functionalities like authentication, authorization, error responses, validation, etc.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .