coders gyan(rest api) ---> book library(1.3)
api creation —> (book → CRUD )
postman—> elib(collection)—>Book(folder)—>Create
Create
—> post —> localhost:5513/api/book
—>body—>form-data(key:value)
//src/book/bookTypes.ts
import { User } from "../user/userTypes";
export interface Book {
_id: string;
title: string;
description: string;
author: User;
genre: string;
coverImage: string;
file: string;
createdAt: Date;
updatedAt: Date;
}
//src/book/bookModel.ts
import mongoose from "mongoose";
import { Book } from "./bookTypes";
const bookSchema = new mongoose.Schema<Book>(
{
title: {
type: String,
required: true,
},
description: {
type: String,
require: true,
},
author: {
type: mongoose.Schema.Types.ObjectId,
// add ref
ref: "User", // logged in user
required: true,
},
coverImage: {
type: String, // url to be stored on cloudniary
required: true,
},
file: {
type: String,
requied: true,
},
genre: {
type: String,
required: true,
},
},
{ timestamps: true } //createdAt , updatedAt
);
// Book --> books
export default mongoose.model<Book>("Book", bookSchema);
npm i multer # to process multi-part form data
npm i @types/multer
//src/book/bookRouter.ts
import path from "node:path";
import express from "express";
import {
createBook,
deleteBook,
getSingleBook,
listBooks,
updateBook,
} from "./bookController";
import multer from "multer";
import authenticate from "../middlewares/authenticate";
const bookRouter = express.Router();
// file store local ->
const upload = multer({
dest: path.resolve(__dirname, "../../public/data/uploads"), // to store image and file
// todo: put limit 10mb max.
limits: { fileSize: 3e7 }, // 30mb 30 * 1024 * 1024
});
// routes
// /api/books
bookRouter.post(
"/",
authenticate, // auth middleware
upload.fields([ // multer middleware --> next() is inbuilt
{ name: "coverImage", maxCount: 1 },
{ name: "file", maxCount: 1 },
]),
createBook
);
bookRouter.patch(
"/:bookId",
authenticate,
upload.fields([
{ name: "coverImage", maxCount: 1 },
{ name: "file", maxCount: 1 },
]),
updateBook
);
bookRouter.get("/", listBooks); // user can view without authetication
bookRouter.get("/:bookId", getSingleBook); // user can view without authetication
bookRouter.delete("/:bookId", authenticate, deleteBook);
export default bookRouter;
npm i cloudinary
// .env
PORT=
MONGO_CONNECTION_STRING=
NODE_ENV=
JWT_SECRET=
CLOUDINARY_CLOUD=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
FRONTEND_DOMAIN=
import { config as conf } from "dotenv";
conf();
const _config = {
port: process.env.PORT,
databaseUrl: process.env.MONGO_CONNECTION_STRING,
env: process.env.NODE_ENV,
jwtSecret: process.env.JWT_SECRET,
cloudinaryCloud: process.env.CLOUDINARY_CLOUD,
cloudinaryApiKey: process.env.CLOUDINARY_API_KEY,
cloudinarySecret: process.env.CLOUDINARY_API_SECRET,
frontendDomain: process.env.FRONTEND_DOMAIN,
};
export const config = Object.freeze(_config);
//src/config/cloudinary.ts
import { v2 as cloudinary } from "cloudinary";
import { config } from "./config";
cloudinary.config({
cloud_name: config.cloudinaryCloud,
api_key: config.cloudinaryApiKey,
api_secret: config.cloudinarySecret,
});
export default cloudinary;
//src/middlewares/authenticate.ts
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { verify } from "jsonwebtoken";
import { config } from "../config/config";
export interface AuthRequest extends Request {
userId: string;
}
// Headers -> Authorization (key) --> Bearer login_token (value)
const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.header("Authorization");
if (!token) {
return next(createHttpError(401, "Authorization token is required."));
}
try {
const parsedToken = token.split(" ")[1]; --> ['Bearer', 'login_token' ]
const decoded = verify(parsedToken, config.jwtSecret as string);
const _req = req as AuthRequest;
_req.userId = decoded.sub as string;
next();
} catch (err) {
return next(createHttpError(401, "Token expired."));
}
};
export default authenticate;
//src/book/bookController.ts
import path from "node:path";
import fs from "node:fs";
import { Request, Response, NextFunction } from "express";
import cloudinary from "../config/cloudinary";
import createHttpError from "http-errors";
import bookModel from "./bookModel";
import { AuthRequest } from "../middlewares/authenticate";
import userModel from "../user/userModel";
const createBook = async (req: Request, res: Response, next: NextFunction) => {
const { title, genre, description } = req.body;
const files = req.files as { [fieldname: string]: Express.Multer.File[] };
// console.log('files',files)
// 'application/pdf'
const coverImageMimeType = files.coverImage[0].mimetype.split("/").at(-1);
const fileName = files.coverImage[0].filename;
const filePath = path.resolve(
__dirname,
"../../public/data/uploads",
fileName
);
try {
// upload image to cloudinary in "book-covers" folder
const uploadResult = await cloudinary.uploader.upload(filePath, {
filename_override: fileName,
folder: "book-covers",
format: coverImageMimeType,
});
const bookFileName = files.file[0].filename;
const bookFilePath = path.resolve(
__dirname,
"../../public/data/uploads",
bookFileName
);
// upload pdf to cloudinary in "book-pdfs" folder
// cloudinary website -->setting -->security --> PDF and file delivery --> check
const bookFileUploadResult = await cloudinary.uploader.upload(
bookFilePath,
{
resource_type: "raw",
filename_override: bookFileName,
folder: "book-pdfs",
format: "pdf",
}
);
const _req = req as AuthRequest;
// store data from database
const newBook = await bookModel.create({
title,
description,
genre,
author: _req.userId, // get uerid from auth middleware
coverImage: uploadResult.secure_url,
file: bookFileUploadResult.secure_url,
});
// Delete temp.files
// todo: wrap in try catch...
await fs.promises.unlink(filePath);
await fs.promises.unlink(bookFilePath);
res.status(201).json({ id: newBook._id });
} catch (err) {
console.log(err);
return next(createHttpError(500, "Error while uploading the files."));
}
};
export { createBook, updateBook, listBooks, getSingleBook, deleteBook };
const updateBook = async (req: Request, res: Response, next: NextFunction) => {
const { title, description, genre } = req.body;
const bookId = req.params.bookId;
const book = await bookModel.findOne({ _id: bookId });
if (!book) {
return next(createHttpError(404, "Book not found"));
}
// Check access
const _req = req as AuthRequest;
if (book.author.toString() !== _req.userId) {
return next(createHttpError(403, "You can not update others book."));
}
// check if image field is exists.
const files = req.files as { [fieldname: string]: Express.Multer.File[] };
let completeCoverImage = "";
if (files.coverImage) {
const filename = files.coverImage[0].filename;
const converMimeType = files.coverImage[0].mimetype.split("/").at(-1);
// send files to cloudinary
const filePath = path.resolve(
__dirname,
"../../public/data/uploads/" + filename
);
completeCoverImage = filename;
const uploadResult = await cloudinary.uploader.upload(filePath, {
filename_override: completeCoverImage,
folder: "book-covers",
format: converMimeType,
});
completeCoverImage = uploadResult.secure_url;
await fs.promises.unlink(filePath);
}
// check if file field is exists.
let completeFileName = "";
if (files.file) {
const bookFilePath = path.resolve(
__dirname,
"../../public/data/uploads/" + files.file[0].filename
);
const bookFileName = files.file[0].filename;
completeFileName = bookFileName;
const uploadResultPdf = await cloudinary.uploader.upload(bookFilePath, {
resource_type: "raw",
filename_override: completeFileName,
folder: "book-pdfs",
format: "pdf",
});
completeFileName = uploadResultPdf.secure_url;
await fs.promises.unlink(bookFilePath);
}
const updatedBook = await bookModel.findOneAndUpdate(
{
_id: bookId, // filter
},
{
title: title,
description: description,
genre: genre,
coverImage: completeCoverImage
? completeCoverImage
: book.coverImage,
file: completeFileName ? completeFileName : book.file,
},
{ new: true }
);
res.json(updatedBook);
};
const listBooks = async (req: Request, res: Response, next: NextFunction) => {
// const sleep = await new Promise((resolve) => setTimeout(resolve, 5000));
try {
// todo: add pagination.
const book = await bookModel.find().populate("author", "name");
res.json(book);
} catch (err) {
return next(createHttpError(500, "Error while getting a book"));
}
};
const getSingleBook = async (
req: Request,
res: Response,
next: NextFunction
) => {
const bookId = req.params.bookId;
try {
const book = await bookModel
.findOne({ _id: bookId })
// populate author field
.populate("author", "name");
if (!book) {
return next(createHttpError(404, "Book not found."));
}
return res.json(book);
} catch (err) {
return next(createHttpError(500, "Error while getting a book"));
}
};
const deleteBook = async (req: Request, res: Response, next: NextFunction) => {
const bookId = req.params.bookId;
const book = await bookModel.findOne({ _id: bookId });
if (!book) {
return next(createHttpError(404, "Book not found"));
}
// Check Access --> auth user can only delete
const _req = req as AuthRequest;
if (book.author.toString() !== _req.userId) {
return next(createHttpError(403, "You can not update others book."));
}
// original string --> https://res.cloudinary.com/degzfrkse/image/upload/v1712590372/book-covers/u4bt9x7sv0r0cg5cuynm.png
// we want this format -->book-covers/dkzujeho0txi0yrfqjsm
const coverFileSplits = book.coverImage.split("/");
const coverImagePublicId =
coverFileSplits.at(-2) + "/" + coverFileSplits.at(-1)?.split(".").at(-2);
const bookFileSplits = book.file.split("/");
const bookFilePublicId =
bookFileSplits.at(-2) + "/" + bookFileSplits.at(-1);
console.log("bookFilePublicId", bookFilePublicId);
// todo: add try error block
await cloudinary.uploader.destroy(coverImagePublicId);
await cloudinary.uploader.destroy(bookFilePublicId, {
resource_type: "raw",
});
await bookModel.deleteOne({ _id: bookId });
return res.sendStatus(204);
};
# cors middleware ---> send the req between 2 diff domain/ports
npm i cors
npm i -D types/cors
# FE (3000 port) ----> BE(5513 port)
// app.ts
import express, { NextFunction, Request, Response } from "express";
import cors from "cors";
import globalErrorHandler from "./middlewares/globalErrorHandler";
import userRouter from "./user/userRouter";
import bookRouter from "./book/bookRouter";
import { config } from "./config/config";
const app = express();
app.use(
cors({
origin: config.frontendDomain,
})
);
app.use(express.json());
// Routes
// Http methods: GET, POST, PUT, PATCH, DELETE
app.get("/", (req, res, next) => {
res.json({ message: "Welcome to elib apis" });
});
app.use("/api/users", userRouter);
app.use("/api/books", bookRouter);
// Global error handler
app.use(globalErrorHandler);
export default app;