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.
Top comments (2)
really interesting , as many developers still avoid typescript in the backend even if they are familiar with it in the frontend
Nice Article