Database: mongoDB
If you prefer NoSQL databases, mongoDB is definitely a great one that you don't want to miss.
Try mongoDB
- Install mongoDB on your machine and start it.
Note: Remember to create indexes on collections based on your need in a production project.
- Add mongo dependency:
npm i mongodb
- Update code.
Use mongoDB for CRUD operations
Add a new domain entity Review
in domain/model/review.ts:
// Review represents the review of a book
export interface Review {
id: string;
book_id: number;
author: string;
title: string;
content: string;
created_at: Date;
updated_at: Date;
}
Declare its business capabilities in domain/gateway/review_manager.ts:
import { Review } from "@/domain/model";
export interface ReviewManager {
createReview(r: Review): Promise<string>;
updateReview(id: string, r: Review): Promise<void>;
deleteReview(id: string): Promise<void>;
getReview(id: string): Promise<Review | null>;
getReviewsOfBook(book_id: number): Promise<Review[]>;
}
Implement these methods in infrastructure/database/mongo.ts:
import { MongoClient, Db, Collection, ObjectId } from "mongodb";
import { Review } from "@/domain/model";
import { ReviewManager } from "@/domain/gateway";
const coll_review = "reviews";
export class MongoPersistence implements ReviewManager {
private db!: Db;
private coll!: Collection;
constructor(mongoURI: string, dbName: string) {
const client = new MongoClient(mongoURI);
client.connect().then(() => {
this.db = client.db(dbName);
this.coll = this.db.collection(coll_review);
console.log("Connected to mongodb.");
});
}
async createReview(r: Review): Promise<string> {
const result = await this.coll.insertOne(r);
return result.insertedId.toHexString();
}
async updateReview(id: string, r: Review): Promise<void> {
const objID = new ObjectId(id);
const updateValues = {
title: r.title,
content: r.content,
updated_at: r.updated_at,
};
const result = await this.coll.updateOne(
{ _id: objID },
{ $set: updateValues }
);
if (result.modifiedCount === 0) {
throw new Error("Review does not exist");
}
}
async deleteReview(id: string): Promise<void> {
const objID = new ObjectId(id);
const result = await this.coll.deleteOne({ _id: objID });
if (result.deletedCount === 0) {
throw new Error("Review does not exist");
}
}
async getReview(id: string): Promise<Review | null> {
const objID = new ObjectId(id);
const reviewDoc = await this.coll.findOne({ _id: objID });
if (!reviewDoc) {
return null;
}
return {
id: reviewDoc._id.toHexString(),
book_id: reviewDoc.book_id,
author: reviewDoc.author,
title: reviewDoc.title,
content: reviewDoc.content,
created_at: reviewDoc.created_at,
updated_at: reviewDoc.updated_at,
};
}
async getReviewsOfBook(book_id: number): Promise<Review[]> {
const cursor = this.coll.find({ book_id });
const reviewDocs = await cursor.toArray();
return reviewDocs.map((reviewDoc) => ({
id: reviewDoc._id.toHexString(),
book_id: reviewDoc.book_id,
author: reviewDoc.author,
title: reviewDoc.title,
content: reviewDoc.content,
created_at: reviewDoc.created_at,
updated_at: reviewDoc.updated_at,
}));
}
}
Add config items for mongodb in infrastructure/config/config.ts:
@@ -3,6 +3,8 @@ import { readFileSync } from "fs";
interface DBConfig {
fileName: string;
dsn: string;
+ mongo_uri: string;
+ mongo_db_name: string;
}
interface ApplicationConfig {
Add config values in config.json:
@@ -4,6 +4,8 @@
},
"db": {
"file_name": "test.db",
- "dsn": "mysql://test_user:test_pass@127.0.0.1:3306/lr_book?charset=utf8mb4"
+ "dsn": "mysql://test_user:test_pass@127.0.0.1:3306/lr_book?charset=utf8mb4",
+ "mongo_uri": "mongodb://localhost:27017",
+ "mongo_db_name": "lr_book"
}
}
Add review_operator
in Application layer, application/executor/review_operator.ts:
import { ReviewManager } from "@/domain/gateway";
import { Review } from "@/domain/model";
import { ReviewBody } from "@/application/dto";
export class ReviewOperator {
private reviewManager: ReviewManager;
constructor(b: ReviewManager) {
this.reviewManager = b;
}
async createReview(rb: ReviewBody): Promise<Review> {
const now = new Date();
const r: Review = {
...rb,
created_at: now,
updated_at: now,
id: "",
};
const id = await this.reviewManager.createReview(r);
r.id = id;
return r;
}
async getReview(id: string): Promise<Review | null> {
return await this.reviewManager.getReview(id);
}
async getReviewsOfBook(book_id: number): Promise<Review[]> {
return await this.reviewManager.getReviewsOfBook(book_id);
}
async updateReview(id: string, r: Review): Promise<Review> {
await this.reviewManager.updateReview(id, r);
return r;
}
async deleteReview(id: string): Promise<void> {
await this.reviewManager.deleteReview(id);
}
}
The ReviewBody
interface is defined in application/dto/review.ts:
export interface ReviewBody {
book_id: number;
author: string;
title: string;
content: string;
}
Tune application/wire_helper.ts to wire in mongodb connections:
@@ -1,16 +1,25 @@
-import { MySQLPersistence } from "@/infrastructure/database";
+import { MySQLPersistence, MongoPersistence } from "@/infrastructure/database";
import { Config } from "@/infrastructure/config";
-import { BookManager } from "@/domain/gateway";
+import { BookManager, ReviewManager } from "@/domain/gateway";
// WireHelper is the helper for dependency injection
export class WireHelper {
- private persistence: MySQLPersistence;
+ private sql_persistence: MySQLPersistence;
+ private no_sql_persistence: MongoPersistence;
constructor(c: Config) {
- this.persistence = new MySQLPersistence(c.db.dsn);
+ this.sql_persistence = new MySQLPersistence(c.db.dsn);
+ this.no_sql_persistence = new MongoPersistence(
+ c.db.mongo_uri,
+ c.db.mongo_db_name
+ );
}
bookManager(): BookManager {
- return this.persistence;
+ return this.sql_persistence;
+ }
+
+ reviewManager(): ReviewManager {
+ return this.no_sql_persistence;
}
}
Add review routes in adapter/router.ts:
@@ -2,14 +2,16 @@ import express, { Request, Response } from "express";
import morgan from "morgan";
import { Book } from "@/domain/model";
-import { BookOperator } from "@/application/executor";
+import { BookOperator, ReviewOperator } from "@/application/executor";
import { WireHelper } from "@/application";
class RestHandler {
private bookOperator: BookOperator;
+ private reviewOperator: ReviewOperator;
- constructor(bookOperator: BookOperator) {
+ constructor(bookOperator: BookOperator, reviewOperator: ReviewOperator) {
this.bookOperator = bookOperator;
+ this.reviewOperator = reviewOperator;
}
// Get all books
@@ -81,21 +83,100 @@ class RestHandler {
res.status(404).json({ error: "Failed to delete" });
}
}
+
+ // Get all book reviews
+ public async getReviewsOfBook(req: Request, res: Response): Promise<void> {
+ const bookID = parseInt(req.params.id, 10);
+ if (isNaN(bookID)) {
+ res.status(400).json({ error: "invalid book id" });
+ return;
+ }
+
+ try {
+ const books = await this.reviewOperator.getReviewsOfBook(bookID);
+ res.status(200).json(books);
+ } catch (err) {
+ console.error(`Failed to get reviews of book ${bookID}: ${err}`);
+ res.status(404).json({ error: "failed to get books" });
+ }
+ }
+
+ // Get single review
+ public async getReview(req: Request, res: Response): Promise<void> {
+ const id = req.params.id;
+
+ try {
+ const review = await this.reviewOperator.getReview(id);
+ res.status(200).json(review);
+ } catch (err) {
+ console.error(`Failed to get the review ${id}: ${err}`);
+ res.status(404).json({ error: "failed to get the review" });
+ }
+ }
+
+ // Create a new review
+ public async createReview(req: Request, res: Response): Promise<void> {
+ const reviewBody = req.body;
+
+ try {
+ const review = await this.reviewOperator.createReview(reviewBody);
+ res.status(201).json(review);
+ } catch (err) {
+ console.error(`Failed to create: ${err}`);
+ res.status(404).json({ error: "failed to create the review" });
+ }
+ }
+
+ // Update an existing review
+ public async updateReview(req: Request, res: Response): Promise<void> {
+ const id = req.params.id;
+ const reqBody = req.body;
+
+ try {
+ const book = await this.reviewOperator.updateReview(id, reqBody);
+ res.status(200).json(book);
+ } catch (err) {
+ console.error(`Failed to update: ${err}`);
+ res.status(404).json({ error: "failed to update the review" });
+ }
+ }
+
+ // Delete an existing review
+ public async deleteReview(req: Request, res: Response): Promise<void> {
+ const id = req.params.id;
+
+ try {
+ await this.reviewOperator.deleteReview(id);
+ res.status(204).send();
+ } catch (err) {
+ console.error(`Failed to delete: ${err}`);
+ res.status(404).json({ error: "failed to delete the review" });
+ }
+ }
}
// Create router
function MakeRouter(wireHelper: WireHelper): express.Router {
const restHandler = new RestHandler(
- new BookOperator(wireHelper.bookManager())
+ new BookOperator(wireHelper.bookManager()),
+ new ReviewOperator(wireHelper.reviewManager())
);
const router = express.Router();
- router.get("", restHandler.getBooks.bind(restHandler));
- router.get("/:id", restHandler.getBook.bind(restHandler));
- router.post("", restHandler.createBook.bind(restHandler));
- router.put("/:id", restHandler.updateBook.bind(restHandler));
- router.delete("/:id", restHandler.deleteBook.bind(restHandler));
+ router.get("/books", restHandler.getBooks.bind(restHandler));
+ router.get("/books/:id", restHandler.getBook.bind(restHandler));
+ router.post("/books", restHandler.createBook.bind(restHandler));
+ router.put("/books/:id", restHandler.updateBook.bind(restHandler));
+ router.delete("/books/:id", restHandler.deleteBook.bind(restHandler));
+ router.get(
+ "/books/:id/reviews",
+ restHandler.getReviewsOfBook.bind(restHandler)
+ );
+ router.get("/reviews/:id", restHandler.getReview.bind(restHandler));
+ router.post("/reviews", restHandler.createReview.bind(restHandler));
+ router.put("/reviews/:id", restHandler.updateReview.bind(restHandler));
+ router.delete("/reviews/:id", restHandler.deleteReview.bind(restHandler));
return router;
}
@@ -115,6 +196,6 @@ export function InitApp(wireHelper: WireHelper): express.Express {
});
const r = MakeRouter(wireHelper);
- app.use("/books", r);
+ app.use("", r);
return app;
}
All the changes are applied now. Let's try these endpoints with curl.
Try with curl
Create a new review:
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"book_id": 1,
"author": "John Doe",
"title": "Great Book",
"content": "This is a great book!"
}' \
http://localhost:3000/reviews
It should respond with this:
{"book_id":1,"author":"John Doe","title":"Great Book","content":"This is a great book!","created_at":"2024-03-01T15:16:19.823Z","updated_at":"2024-03-01T15:16:19.823Z","id":"65e1f143f1c5f50b36b2ce60","_id":"65e1f143f1c5f50b36b2ce60"}
Fetch a single review by ID:
curl -X GET http://localhost:3000/reviews/65e1f143f1c5f50b36b2ce60
Result:
{
"id": "65e1f143f1c5f50b36b2ce60",
"book_id": 1,
"author": "John Doe",
"title": "Great Book",
"content": "This is a great book!",
"created_at": "2024-03-01T15:16:19.823Z",
"updated_at": "2024-03-01T15:16:19.823Z"
}
List all reviews of a book:
curl -X GET http://localhost:3000/books/1/reviews
Result list:
[
{
"id": "65e1f143f1c5f50b36b2ce60",
"book_id": 1,
"author": "John Doe",
"title": "Great Book",
"content": "This is a great book!",
"created_at": "2024-03-01T15:16:19.823Z",
"updated_at": "2024-03-01T15:16:19.823Z"
},
{
"id": "65e1f1c0f1c5f50b36b2ce61",
"book_id": 1,
"author": "Carl Smith",
"title": "Best best Book",
"content": "This is a great book!",
"created_at": "2024-03-01T15:18:24.124Z",
"updated_at": "2024-03-01T15:18:24.124Z"
}
]
Update an existing review
curl -X PUT \
-H "Content-Type: application/json" \
-d '{
"content": "I prefer Robert Smith new book",
"title": "Not that good"
}' \
http://localhost:3000/reviews/65e1f143f1c5f50b36b2ce60
Result:
{"content":"I prefer Robert Smith new book","title":"Not that good"}
Delete an existing review:
curl -X DELETE http://localhost:3000/reviews/65e1f143f1c5f50b36b2ce60
It returns code 204 for a sucessful deletion.
Voila! Your api server is using mongoDB as well now!