
In this tutorial, we are making API for a Movie Portal. Here we use NodeJs, Express-5.0, and other necessary Packages. We are Trying to Follow the MVC Pattern.
Step 1 : Scaffolding
Open your terminal and type
npm init
Fill up your required field. Then open your visual studio or any kind of editor. Create .env file type this-
PORT= 8000DATABASE_URL = "mongodb://localhost:27017"IMAGE_BASE_URL = "http://127.0.0.1:8000"DBNAME = "exress_movie_api"
Then create the following folders-
- controllers
- db
- routes
- helpers
- middleware
- public / uploads
- category
- movie
Now install this app-
- express
- cors
- dotenv
- joi
- bcryptjs
- jsonwebtoken
- mongoose
- multer
- nodemon
- uuid
And here, in your package.json file add“type”: “module” because we are using express-5.0. if you don’t do that in your .js file import is not work.
After That, create an app.js file in your root folder. Here you type this –
import cors from 'cors';import dotenv from 'dotenv';import express from 'express';import connectiondb from './db/connectiondb.js';dotenv.config();const app = express();const port = process.env.PORT || 8000;const DATABASE_URL = process.env.DATABASE_URL || "mongodb:://localhost:27017";//connection databaseconnectiondb(DATABASE_URL);//app use for different purposeapp.use(cors());app.use(express.json());app.use(express.urlencoded({ extended:true}))app.listen(port, ()=>{ console.log(`server listening at http://localhost:${port}`)})
Now create db folder insideconnectiondb.js file and type this-
import mongoose from 'mongoose';//database connectionconst connectiondb = async(DATABASE_URL)=>{ try{ const options = { dbName: process.env.DBNAME } const connectionResult = await mongoose.connect(DATABASE_URL, options); console.log(`connection to mongoDB on database: ${connectionResult.connections[0].name} at ${new Date().toDateString()}`); }catch(err){ console.log(err); }}export default connectiondb;
Now last. Open your MongoDB and Create a Database. Database Name will be“express_movie_api” and Collection Name is “movies”;
Now open your terminal and type npm run dev. You can see this-
If you see your database name and current date then your connection will be ok. You are now going to the next step.
Step-2 : Model Defined
In your models folder make these files.
- User.js
- Category.js
- Movie.js
And Type this.
User.js
import mongoose from "mongoose";const DataSchema = new mongoose.Schema({ name: { type: String, trim: true, required: true, maxlength: 32, }, email: { type: String, trim: true, required: true, unique: true, }, password: { type: String, required: true },},{ timestamps: true, versionKey:false })const User = mongoose.model('User', DataSchema);export default User;
Category.js
import mongoose from "mongoose";const DataSchema = new mongoose.Schema({ name: { type: String, required: [true, 'Please enter name'], trim: true, maxLength: [100, 'Category name cannot exceed 100 characters'] }, image: { type: Object, },},{ timestamps: true, versionKey:false })const Category = mongoose.model('Category', DataSchema);export default Category;
Movie.js
import mongoose from "mongoose";const DataSchema = new mongoose.Schema({ title: { type: String, required: [true, 'Please enter name'], trim: true, maxLength: [100, 'Category name cannot exceed 100 characters'] }, category_id: { type: mongoose.Types.ObjectId, ref: "Category", required: true }, descriptin: { type: String, trim: true, }, image: { type: String, required: true },},{ timestamps: true, versionKey:false })const Movie = mongoose.model('Movie', DataSchema);export default Movie;
Step-3 : Helpers Function define
Now you create some auxiliary function. This functions as some kind of middleware. Create this-
- bcryp.js
- jwt.js
bcryp.js Function Helping client password encoded and decoded.
import bcrypt from 'bcryptjs';export const hashMaker = (str)=> { const salt = bcrypt.genSaltSync(10); return bcrypt.hashSync(str, salt);}export const matchData = (str, hash)=> { return bcrypt.compareSync(str, hash);}
jwt.js function helping Jwt token create and decoded this token.
import jwt from 'jsonwebtoken';const secret = "@#$%^&*&*"export const createToken = (payload)=> { return jwt.sign(payload, secret, { expiresIn: '1d' }); //1day}export const decodeToken = (payload)=> { return jwt.verify(payload, secret);}
Step-4: Making Some Middleware.
In your middleware folder create some middleware file. Like this-
- auth.js
- fileFolderName.js
- fileUpload.js
auth.js
import { decodeToken } from '../helpers/jwt.js';const auth = (req, res, next)=> { try { const token = req.headers.authorization.split(' ')[1]; req.user = decodeToken(token); //console.log(req.user) next(); } catch (err) { res.status(400).send('Authentication Failed') }}export default auth;
fileFolderName.js
const fileFloderName = (folderName) =>{ return ( req, res, next)=>{ req.folderName = folderName; next(); }}export default fileFloderName
fileUpload.js
import multer from 'multer';const storage = multer({ storage: multer.diskStorage({ destination: function (req, file, cb) { const folderName = req.folderName ?? file.fieldname; cb(null, `public/uploads/${folderName}`); }, filename: function (req, file, cb) { cb(null, `${Date.now()}-${file.originalname}`); }, }), fileFilter: function (req, file, cb) { const allowedMimeTypes = ['image/jpeg', 'image/png']; // Specify the allowed image file types if (allowedMimeTypes.includes(file.mimetype)) { cb(null, true); // Accept the file } else { cb(new Error('Invalid file type. Only JPEG and PNG files are allowed.')); // Reject the file } }, limits: { fileSize: 2 * 1024 * 1024, // Set the maximum file size (2MB in this example) },});export default storage;
After that Create validation folder for validation field and then create this file
- userSingupValidator.js
- categoryStoreValidator.js
- movieStoreValidator.js
userSingupValidator.js
import Joi from 'joi';export const userSignupValidator = (req, res, next) => { const schema = Joi.object({ name: Joi.string().required().messages({ 'any.required': 'Name is required', }), email: Joi.string().email().required().messages({ 'string.email': 'Email must be a valid email address', 'any.required': 'Email is required', }), password: Joi.string() .min(6) .required() .pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')) .messages({ 'string.min': 'Password must be at least 6 characters', 'any.required': 'Password is required', 'string.pattern.base': 'Password must contain only alphanumeric characters', }), password_confirmation: Joi.string() .valid(Joi.ref('password')) .required() .messages({ 'any.only': 'Password confirmation must match the password', 'any.required': 'Password confirmation is required', }), job: Joi.string().optional(), }); const { error } = schema.validate(req.body); if (error) { const errorMessage = error.details[0].message; return res.status(400).json({ error: errorMessage }); } next();};
categoryStoreValidator.js
import Joi from 'joi';export const categoryStoreValidator = (req, res, next) => { const schema = Joi.object({ name: Joi.string() .required(). messages({ 'string.name': 'Name is required' }), }); const { error } = schema.validate(req.body); if (error) { const errorMessage = error.details[0].message; return res.status(400).json({ error: errorMessage }); } next();};
movieStoreValidator.js
import Joi from 'joi';export const movieStoreValidator = (req, res, next) => { const schema = Joi.object({ title: Joi.string() .required(). messages({ 'string.name': 'Name is required' }), category_id: Joi.string() .required(). messages({ 'string.category_id': 'Category Name is required' }), image: Joi.string() .required(). messages({ 'string.image': 'image is required' }), }); const { error } = schema.validate(req.body); if (error) { const errorMessage = error.details[0].message; return res.status(400).json({ error: errorMessage }); } next();};
Step-5 : Route define
In your routes folder create this file-
- rootRoute.js
- authRoute.js
- categoryRoute.js
- movieRoute.js
rootRoute.js
In this file you add this code-
import express from 'express';import multer from 'multer';import CategoryRoute from './CategoryRoute.js';import authRoute from './authRoute.js';import movieRoute from './movieRoute.js';const route = (app)=>{ app.use(express.static("public/uploads/category")); app.use(express.static("public/uploads/movie")); app.use("/api", authRoute); app.use("/api", CategoryRoute); app.use("/api", movieRoute); //Multer Error File Handling app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { // Multer-specific errors return res.status(418).json({ err_code: err.code, err_message: err.message, }); } else { // Handling errors for any other cases from whole application return res.status(500).json({ err_code: 409, err_message: "Something went wrong!", }); } }); //unknown route app.use('*', (req, res)=>{ res.status(404).json({status:"fail", data:"Route does not exists"}) })}export default route;
here first we define static folder for save image.
app.use(express.static("public/uploads/category"));app.use(express.static("public/uploads/movie"));
Like category image and movie image. Then write error handling image function-
//Multer Error File Handling app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { // Multer-specific errors return res.status(418).json({ err_code: err.code, err_message: err.message, }); } else { // Handling errors for any other cases from whole application return res.status(500).json({ err_code: 409, err_message: "Something went wrong!", }); } });
At the end we write unknown route handler function. If user didn’t write proper url then show message“Route does not exists”
//unknown route app.use('*', (req, res)=>{ res.status(404).json({status:"fail", data:"Route does not exists"}) })
Now go toapp.js file and add this line-
import rootRoute from './routes/rootRoute.js';rootRoute(app);
like this way-
import cors from 'cors';import dotenv from 'dotenv';import express from 'express';import connectiondb from './db/connectiondb.js';import rootRoute from './routes/rootRoute.js';dotenv.config();const app = express();const port = process.env.PORT || 8000;const DATABASE_URL = process.env.DATABASE_URL || "mongodb:://localhost:27017";//connection databaseconnectiondb(DATABASE_URL);//app use for different purposeapp.use(cors());app.use(express.json());app.use(express.urlencoded({ extended:true}))rootRoute(app);app.listen(port, ()=>{ console.log(`server listening at http://localhost:${port}`)})
Again we go torootRoute.js file. Here we first discuss about authRoute which we are importing from this
import authRoute from './authRoute.js';
so, createauthRoute.js file in the routes folder and write this code-
import express from 'express';import AuthController from '../controllers/AuthController.js';import auth from '../middleware/auth.js';import { userSignupValidator } from '../middleware/validation/userSingupValidator.js';const router = express.Router();router.post('/register', userSignupValidator, AuthController.registration);router.post('/login', AuthController.login);router.post('/logout', AuthController.logout);router.post('/change-password', auth, AuthController.changePassword);export default router;
Then go to the controllers folder createAuthController.js file and write this-
import { hashMaker, matchData } from "../helpers/bcryp.js";import { createToken } from "../helpers/jwt.js";import User from "../models/User.js";class UserController { static registration = async (req, res) => { try { const { name, email, password, password_confirmation } = req.body; const existEmail = await User.findOne({ email }).lean().exec(); if (existEmail) { return res.status(413).json({ code: 201, message: "This Email is already used.", }); } const hashPassword = hashMaker(password); const registerUser = await new User({ name: name, email: email, password: hashPassword, }).save(); console.log("userEmail", hashPassword); res.status(201).json({ code: 201, message: "User registration Successfully.", payload: registerUser, }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } } static login = async (req, res) =>{ try { const { email, password } = req.body; const existEmail = await User.findOne({ email }).lean().exec(); //mail check if (!existEmail) { return res.status(413).json({ code: 201, message: "crediential didn't match", }); } const hashPassword = matchData(password, existEmail.password); //password check if(!hashPassword){ return res.status(413).json({ code: 201, message: "crediential didn't match", }); } const {_id, name} = existEmail const payload ={ name, email, token: "Bearer " + createToken({ _id, email }) } res.status(200).json({ code: 200, message: "User Login Successfully.", payload }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } } static logout = (req, res)=>{ try { // Clear the token from the cookie res.clearCookie('token'); res.status(200).json({ code: 200, message: 'User logged out successfully.', }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } } static changePassword = async(req, res)=>{ const {password, password_confirmation } = req.body; try{ const {_id} = req.user; if (password !== password_confirmation) { return res.status(413).json({ code: 201, message: "crediential didn't match", }); } const userInfo = await User.findById(_id); const hashPassword = hashMaker(password); userInfo.password = hashPassword; await userInfo.save(); console.log(userInfo) //await User.updateOne({ _id: _id }, { $set: payload }, { upsert: false }); res.status(200).json({ code: 200, message: "passwrod Change Successfully.", }); }catch(err){ console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } }}export default UserController;
Is your output similar to this congratulation, you have successfully user registration. Now we are testing the login route.
If you log in successfully then we can see the token. This token is a jwt token. We can now use this token as an authentication.
Now we go to again rootRoute.js and this time we are talking about CategoryRoute.js. So create this file and write this
CategoryRoute.js
import express from 'express';import CategoryController from '../controllers/CategoryController.js';import fileFolderName from '../middleware/fileFolderName.js';import storage from '../middleware/fileUpload.js';import { categoryStoreValidator } from "../middleware/validation/categoryStoreValidator.js";const router = express.Router();router.get("/all-category", CategoryController.allCategory);router.get("/category-wise-movies", CategoryController.categoryWiseMovie);router.get("/single-category/:categoryId", CategoryController.singleCategory);router.post( "/category-store", //auth, categoryStoreValidator, fileFolderName("category"), storage.fields([{ name: "image", maxCount: 1 }]), CategoryController.store);router.put( "/category-update/:categoryId", //auth, categoryStoreValidator, fileFolderName("category"), storage.fields([{ name: "image", maxCount: 1 }]), CategoryController.update);router.delete( "/category-delete/:categoryId", //auth, //categoryStoreValidator, fileFolderName("category"), storage.fields([{ name: "image", maxCount: 1 }]), CategoryController.delete);// //find product by idrouter.param("categoryId", CategoryController.categoryById);export default router;
Here you see, we write router.param("categoryId", CategoryController.categoryById). This route is a special route, like when we call single route –
router.get("/single-category/:categoryId", CategoryController.singleCategory);
here this route we passed categoryId param. So this param first calls router. param route and after that call single-category route. It helps for reduce query.
Now we go to the controllers folder createCategoryController.js and write this
import fs from "fs";import Category from "../models/Category.js";import Movie from "../models/Movie.js";class CategoryController { //find a Category by id static categoryById = async (req, res, next, id) => { const category = await Category.findById(id); if (!category) { return res.status(404).json({ code: 404, message: "Category not found.", }); } req.category = category; // console.log('cat result', req.category) next(); }; //all Category static allCategory = async (req, res) => { try { const imageBaseURL = process.env.IMAGE_BASE_URL; const allCategory = await Category.find(); // Update the image URL for each category const updatedCategoryList = allCategory.map((category) => { return { ...category._doc, image: `${imageBaseURL}/${category.image}`, }; }); res.status(200).json({ message: "All Category", data: updatedCategoryList, }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } }; //Category wise movies static categoryWiseMovie = async (req, res) => { try { const imageBaseURL = process.env.IMAGE_BASE_URL; const allCategories = await Category.find(); // Update the image URL for each category const updatedCategoryList = await Promise.all(allCategories.map(async (category) => { // Find all movies related to this category const movies = await Movie.find({ category_id: category._id }); // Update the image URL for each movie const updatedMovies = movies.map((movie) => { return { ...movie._doc, image: `${imageBaseURL}/${movie.image}`, }; }); return { ...category._doc, image: category.image ? `${imageBaseURL}/${category.image}` : null, movies: updatedMovies, }; })); res.status(200).json({ message: "Category-wise Movies", data: updatedCategoryList, }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); }}; //singel Category static singleCategory = async (req, res) => { const imageBaseURL = process.env.IMAGE_BASE_URL; const category = req.category; category.image = `${imageBaseURL}/${category.image}`; res.status(200).json({ message: "Single Category", data: category, }); }; //store category static store = async (req, res) => { const { name } = req.body; const image = req.files.image[0].filename; try { // const categoryInfo = await new Category({ // name, // image: image ?? '' // }).save() console.log("controller req", image); const categoryInfo = await Category.create({ name, image: image, }); res.status(200).json({ message: "Category Create Successfully", data: categoryInfo, }); } catch (err) { res.status(500).json({ code: 500, message: "Internal server error.", }); } }; //Category update or edit static update = async (req, res) => { const { name } = req.body; const imageBaseURL = process.env.IMAGE_BASE_URL; let image = null; // Initialize image as null let existingCategory = req.category; try { if (req.files && req.files.image) { // If a new image then delete the old image if (existingCategory && existingCategory.image) { fs.unlinkSync(`public/uploads/category/${existingCategory.image}`); } // Save the new image image = req.files.image[0].filename; } existingCategory.name = name ? name : existingCategory.name; existingCategory.image = image ? image : existingCategory.image; existingCategory.save(); // Update the image URL with the base URL const updateCategory = await Category.findById(existingCategory._id); updateCategory.image = `${imageBaseURL}/${updateCategory.image}`; res.status(200).json({ message: "Category updated successfully.", data: updateCategory, }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } }; //delete category static delete = async (req, res) => { try { // Find the category by ID const category = req.category; // Delete the category's image file if it exists if (category.image) { fs.unlinkSync(`public/uploads/category/${category.image}`); } // Delete the category from the database await Category.deleteOne({ _id: category._id }); res.status(200).json({ message: "Category deleted successfully.", }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } };}export default CategoryController;
Now again open your postman and test this api like this way
Now we are discussing movie route. So go to routes folder and create this-
movieRoute.js
import express from "express";import MovieController from "../controllers/MovieController.js";import fileFolderName from "../middleware/fileFolderName.js";import storage from "../middleware/fileUpload.js";import { movieStoreValidator } from "../middleware/validation/movieStoreValidator.js";import auth from '../middleware/auth.js';const router = express.Router();router.get("/all-movies", MovieController.allMovie);router.get("/top-movies", MovieController.topMovie);router.get("/single-movie/:movieId", MovieController.singleMovie); router.post( "/movie-store", //auth, movieStoreValidator, fileFolderName("movie"), storage.fields([{ name: "image", maxCount: 1 }]), MovieController.store ); router.post( "/ai-movie-store", //auth, fileFolderName("movie"), storage.fields([{ name: "image", maxCount: 1 }]), MovieController.aiStore ); router.put( "/movie-update/:movieId", //auth, fileFolderName("movie"), storage.fields([{ name: "image", maxCount: 1 }]), MovieController.update ); router.delete( "/movie-delete/:movieId", //auth, MovieController.delete );// //find product by id router.param("movieId", MovieController.movieById); export default router;
and then go to controllers folder and create this-
MovieController.js
import fs from "fs";import { v4 } from "uuid";import Movie from '../models/Movie.js';class MovieController{ //find a movie by id static movieById = async (req, res, next, id) => { const movie = await Movie.findById(id); console.log('movie info', movie) if (!movie) { return res.status(404).json({ code: 404, message: "Movie not found.", }); } req.movie = movie; // console.log('cat result', req.category) next(); }; //all Movie static allMovie = async (req, res) => { try { const imageBaseURL = process.env.IMAGE_BASE_URL; const allMovie = await Movie.find().populate('category_id'); // Update the image URL for each category const updatedMovieList = allMovie.map((movie) => { return { ...movie._doc, image: `${imageBaseURL}/${movie.image}`, category_id: { ...movie.category_id._doc, image: `${imageBaseURL}/${movie.category_id.image}`, }, }; }); res.status(200).json({ message: "All Movie", data: updatedMovieList, }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } }; //Top Movie static topMovie = async (req, res) => { try { const imageBaseURL = process.env.IMAGE_BASE_URL; const topMovies = await Movie.find() .sort({ createdAt: -1 }) .populate('category_id') .limit(8); // Update the image URL for each movie const updatedTopMovies = topMovies.map((movie) => { return { ...movie._doc, image: `${imageBaseURL}/${movie.image}`, category_id: { ...movie.category_id._doc, image: `${imageBaseURL}/${movie.category_id.image}`, }, }; }); res.status(200).json({ message: "Top 8 Movies", data: updatedTopMovies, }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } }; //singel Category static singleMovie = async (req, res) => { const imageBaseURL = process.env.IMAGE_BASE_URL; const movie = req.movie; await movie.populate('category_id'); movie.image = `${imageBaseURL}/${movie.image}`; movie.category_id.image = `${imageBaseURL}/${movie.category_id.image}`; res.status(200).json({ message: "Single movie", data: movie, }); }; //store Movie static store = async (req, res) => { const { title, discription, category_id } = req.body; const image = req.files.image[0].filename; try { const movieInfo = await Movie.create({ title, discription, category_id, image: image, }); res.status(200).json({ message: "Movie Create Successfully", data: movieInfo, }); } catch (err) { res.status(500).json({ code: 500, message: "Internal server error.", }); } }; //Ai movie store static aiStore = async (req, res) => { const { title, category_id, description, image } = req.body; console.log('node-1', req.body) // Convert the base64 image data to a buffer const imageBuffer = Buffer.from(image, 'base64'); // Generate a unique filename for the image using UUID const imageName = `${v4()}.jpg`; try { // Save the image to a directory (you can change the path as needed) fs.writeFileSync(`./public/uploads/movie/${imageName}`, imageBuffer); // Save the movie information to the database const movieInfo = await Movie.create({ title, description, category_id, image: imageName, }); res.status(200).json({ message: "Movie Create Successfully", data: movieInfo, }); } catch (err) { res.status(500).json({ code: 500, message: "Internal server error.", }); } }; //Category update or edit static update = async (req, res) => { const { title, discription, category_id } = req.body; const imageBaseURL = process.env.IMAGE_BASE_URL; let image = null; // Initialize image as null let existingMovie = req.movie; try { if (req.files && req.files.image) { // If a new image then delete the old image if (existingMovie && existingMovie.image) { fs.unlinkSync(`public/uploads/movie/${existingMovie.image}`); } // Save the new image image = req.files.image[0].filename; } console.log('movie data', title, discription, category_id) existingMovie.title = title ? title : existingMovie.title; existingMovie.discription = discription ? discription : existingMovie.discription; existingMovie.category_id = category_id ? category_id : existingMovie.category_id; existingMovie.image = image ? image : existingMovie.image; existingMovie.save(); console.log("existingMovie", existingMovie) // Update the image URL with the base URL const updateMovie = await Movie.findById(existingMovie._id); updateMovie.image = `${imageBaseURL}/${updateMovie.image}`; res.status(200).json({ message: "Movie updated successfully.", data: updateMovie, }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } }; //delete category static delete = async (req, res) => { try { // Find the category by ID const movie = req.movie; // Delete the category's image file if it exists if (movie.image) { fs.unlinkSync(`public/uploads/movie/${movie.image}`); } // Delete the category from the database await movie.deleteOne({ _id: movie._id }); res.status(200).json({ message: "movie deleted successfully.", }); } catch (err) { console.log(err); res.status(500).json({ code: 500, message: "Internal server error.", }); } };}export default MovieController
Now we do finish touch. Open postman and test it, like this-
So, our 1st part is done. Now we will do 2nd part
Scaffolding redux-toolkit in NextJs.Link
full Project github
Node
https://github.com/kamruzzamanripon/node-movie-api
NextJs
https://github.com/kamruzzamanripon/next-movie-ui-with-node-api
NextJs UI
https://github.com/kamruzzamanripon/next-movie-ui
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse