DEV Community

Cover image for Building a TypeScript REST API with an Object-Oriented Programming (OOP) Approach
Abayomi Ogunnusi
Abayomi Ogunnusi

Posted on

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

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.

Top comments (2)

Collapse
 
oussamabouyahia profile image
Oussama Bouyahia

really interesting , as many developers still avoid typescript in the backend even if they are familiar with it in the frontend

Collapse
 
josephuzuegbu profile image
Joseph Uzuegbu

Nice Article