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
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
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
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
Step 4: Initialize the project
Run the following command to initialize the project:
npm init -y
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
Step 6: Create a tsconfig.json file
tsc --init
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
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;
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;
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[]>;
}
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;
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();
}
}
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;
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;
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;
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;
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;
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;
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();
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"
}
Step 21: Run the application
npm run dev
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
### Create a new user
POST http://localhost:3000/api/v1/users
Content-Type: application/json
{
"name": "John Doe",
"email": "luli@yopmail.com",
"password": "password"
}
### Get all users
GET http://localhost:3000/api/v1/users
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.