├── .dockerignore ├── .env.example ├── src ├── configs │ └── db.ts ├── routes │ ├── api.ts │ ├── blogRoutes.ts │ └── userRoutes.ts ├── services │ ├── blogService.ts │ └── userService.ts ├── validators │ ├── blog.validator.ts │ └── user.validator.ts ├── models │ ├── blog.ts │ └── user.ts ├── middlewares │ └── verifyUserToken.ts ├── serializers │ └── serializers.ts └── controllers │ ├── userController.ts │ ├── blogController.ts │ └── authController.ts ├── Dockerfile ├── .eslintrc.js ├── docker-compose.yml ├── Makefile ├── tests └── homeTest.ts ├── LICENSE ├── index.ts ├── package.json ├── .gitignore ├── README.md └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="mongodb://localhost:27017/dev" 2 | JWT_SECRET="4gFk^2-4]DrX8xEjvis{GMm[tM3W(y" 3 | -------------------------------------------------------------------------------- /src/configs/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | export const connectDB = async () => { 3 | await mongoose 4 | .connect(process.env.DATABASE_URL!) 5 | .then(() => { 6 | console.log("Successfully connected to the database"); 7 | }) 8 | .catch((err) => { 9 | console.log("Error connecting to the database"); 10 | console.log(err); 11 | process.exit(); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/routes/api.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import blogRouter from "./blogRoutes"; 3 | import userRouter from "./userRoutes"; 4 | 5 | const apiRouter = express.Router(); 6 | 7 | apiRouter.get("/", (req: Request, res: Response) => { 8 | res.status(200).send({ message: "Welcome to your Express App API." }); 9 | }); 10 | apiRouter.use("/blogs", blogRouter); 11 | apiRouter.use("/user", userRouter); 12 | export default apiRouter; 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | # Create app directory 3 | WORKDIR /usr/src/app 4 | 5 | # Install app dependencies 6 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 7 | # where available (npm@5+) 8 | COPY package*.json ./ 9 | 10 | RUN npm install 11 | # RUN npm run build 12 | # If you are building your code for production 13 | # RUN npm ci --only=production 14 | 15 | # Bundle app source 16 | COPY . . 17 | 18 | EXPOSE 3000 19 | 20 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "overrides": [ 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint" 19 | ], 20 | "rules": { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | node: 4 | restart: always 5 | build: . 6 | ports: 7 | - 3000:3000 8 | environment: 9 | - DATABASE_URL=mongodb://mongo:27017/prod 10 | volumes: 11 | - ./:/code 12 | mongo: 13 | image: mongo 14 | ports: 15 | - 27017:27017 16 | volumes: 17 | - mongodb:/data/db 18 | mongo-express: 19 | image: mongo-express 20 | restart: always 21 | depends_on: 22 | - mongo 23 | ports: 24 | - 8081:8081 25 | volumes: 26 | mongodb: 27 | -------------------------------------------------------------------------------- /src/services/blogService.ts: -------------------------------------------------------------------------------- 1 | import Blog from "../models/blog"; 2 | 3 | export const BlogService = { 4 | getAllBlogs: async () => { 5 | return await Blog.find(); 6 | }, 7 | 8 | createBlog: async (blog: any) => { 9 | return await Blog.create(blog); 10 | }, 11 | getBlogById: async (id: any) => { 12 | return await Blog.findById(id); 13 | }, 14 | 15 | updateBlog: async (id: any, blog: any) => { 16 | return await Blog.findByIdAndUpdate(id, blog); 17 | }, 18 | 19 | deleteBlog: async (id: any) => { 20 | return await Blog.findByIdAndDelete(id); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/services/userService.ts: -------------------------------------------------------------------------------- 1 | import User from "../models/user"; 2 | 3 | export const userService = { 4 | getAllUsers: async () => { 5 | return await User.find(); 6 | }, 7 | 8 | createUser: async (user: any) => { 9 | return await User.create(user); 10 | }, 11 | getUserById: async (id: any) => { 12 | return await User.findById(id); 13 | }, 14 | 15 | updateUser: async (id: any, user: any) => { 16 | return await User.findByIdAndUpdate(id, user); 17 | }, 18 | 19 | deleteUser: async (id: any) => { 20 | return await User.findByIdAndDelete(id); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/validators/blog.validator.ts: -------------------------------------------------------------------------------- 1 | import { body } from "express-validator"; 2 | 3 | export const createBlogDataValidator = [ 4 | body("title") 5 | .exists({ checkFalsy: true }) 6 | .withMessage("Title is required") 7 | .isString() 8 | .withMessage("Title should be string"), 9 | body("description") 10 | .exists() 11 | .withMessage("Description is required") 12 | .isString() 13 | .withMessage("Description should be string"), 14 | ]; 15 | 16 | export const updateBlogDataValidator = [ 17 | body("title").isString().withMessage("Title should be string"), 18 | body("description").isString().withMessage("Description should be string"), 19 | ]; 20 | -------------------------------------------------------------------------------- /src/models/blog.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | const Schema = mongoose.Schema; 3 | 4 | const blogSchema = new Schema( 5 | { 6 | title: { 7 | type: String, 8 | required: [true, "Blog title required"], 9 | unique: true, 10 | }, 11 | description: { 12 | type: String, 13 | required: [true, "Blog description required"], 14 | }, 15 | image: String, 16 | tags: { 17 | type: Array, 18 | required: false, 19 | }, 20 | author: { 21 | type: mongoose.Schema.Types.ObjectId, 22 | ref: "User", 23 | }, 24 | }, 25 | { 26 | timestamps: true, 27 | } 28 | ); 29 | const Blog = mongoose.model("Blog", blogSchema); 30 | export default Blog; 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | SHELL = /bin/bash 3 | 4 | .PHONY: help 5 | help: 6 | @echo "Commands:" 7 | @echo "lint : executes eslint." 8 | @echo "clean : cleans all docker images" 9 | @echo "test : execute tests" 10 | @echo "serve : run server" 11 | 12 | # Styling 13 | .PHONY: lint 14 | lint: 15 | npm run lint 16 | 17 | # Cleaning 18 | .PHONY: clean 19 | clean: 20 | rm -rf dist 21 | docker image prune --all --force 22 | 23 | # docker compose up 24 | up: 25 | npm run build 26 | docker-compose up -d 27 | 28 | # docker compose down 29 | down: 30 | docker-compose down 31 | 32 | #docker ps 33 | ps: 34 | docker ps 35 | 36 | # Serve 37 | .ONESHELL: 38 | serve: 39 | npm run dev 40 | 41 | # test 42 | .ONESHELL: 43 | test: 44 | npm run test -------------------------------------------------------------------------------- /src/routes/blogRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | getAllBlogs, 4 | createBlog, 5 | getBlogById, 6 | updateBlog, 7 | deleteBlog, 8 | } from "../controllers/blogController"; 9 | import { verifyUserToken } from "../middlewares/verifyUserToken"; 10 | import { 11 | createBlogDataValidator, 12 | updateBlogDataValidator, 13 | } from "../validators/blog.validator"; 14 | 15 | const blogRouter = express.Router(); 16 | 17 | blogRouter 18 | .route("/") 19 | .get(getAllBlogs) 20 | .post(verifyUserToken, createBlogDataValidator, createBlog); 21 | blogRouter 22 | .route("/:id") 23 | .get(getBlogById) 24 | .put(verifyUserToken, updateBlogDataValidator, updateBlog) 25 | .delete(verifyUserToken, deleteBlog); 26 | export default blogRouter; 27 | -------------------------------------------------------------------------------- /src/routes/userRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { AuthController } from "../controllers/authController"; 3 | import { UserController } from "../controllers/userController"; 4 | import { verifyUserToken } from "../middlewares/verifyUserToken"; 5 | import { 6 | createUserDataValidator, 7 | loginUserDataValidator, 8 | } from "../validators/user.validator"; 9 | 10 | const userRouter = express.Router(); 11 | 12 | userRouter.post( 13 | "/register", 14 | createUserDataValidator, 15 | AuthController.registerUser 16 | ); 17 | userRouter.post("/login", loginUserDataValidator, AuthController.loginUser); 18 | userRouter.get("/profile", verifyUserToken, AuthController.getUser); 19 | userRouter.get("/list", verifyUserToken, UserController.getAllUsers); 20 | 21 | export default userRouter; 22 | -------------------------------------------------------------------------------- /tests/homeTest.ts: -------------------------------------------------------------------------------- 1 | import chai from "chai"; 2 | import chaiHttp from "chai-http"; 3 | import app from "../index"; 4 | 5 | chai.should(); 6 | 7 | chai.use(chaiHttp); 8 | 9 | describe("Express API", () => { 10 | describe("/GET Home", () => { 11 | it("it should GET home page of API", (done) => { 12 | chai 13 | .request(app) 14 | .get("/") 15 | .end((err, res) => { 16 | res.should.have.status(200); 17 | done(); 18 | }); 19 | }); 20 | }); 21 | describe("/GET Home", () => { 22 | it("it should GET v1 page of API", (done) => { 23 | chai 24 | .request(app) 25 | .get("/api/v1/") 26 | .end((err, res) => { 27 | res.should.have.status(200); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/middlewares/verifyUserToken.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { Request, Response, NextFunction } from "express"; 3 | 4 | export const verifyUserToken = ( 5 | req: any, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | if (!req.headers.authorization) { 10 | return res 11 | .status(401) 12 | .send({ status: "error", message: "Unauthorized request" }); 13 | } 14 | const token = req.headers["authorization"].split(" ")[1]; 15 | if (!token) { 16 | return res 17 | .status(401) 18 | .send({ status: "error", message: "Access denied. No token provided." }); 19 | } 20 | try { 21 | const decoded: any = jwt.verify(token, process.env.JWT_SECRET!); 22 | req.user = decoded; 23 | next(); 24 | } catch (err) { 25 | res.status(400).send({ status: "error", message: "Invalid token." }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 BHIMRAJ YADAV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/serializers/serializers.ts: -------------------------------------------------------------------------------- 1 | const abstractSerializer = (dict: any, fields: string[]) => { 2 | const data = Object(); 3 | fields.forEach((key: any) => { 4 | const k: string = key; 5 | const v: string = dict[key]; 6 | data[k] = v; 7 | }); 8 | return data; 9 | }; 10 | const userFields: string[] = ["_id", "name", "email", "role"]; 11 | 12 | const blogFields: string[] = [ 13 | "_id", 14 | "title", 15 | "description", 16 | "image", 17 | "tags", 18 | "author", 19 | "createdAt", 20 | "updatedAt", 21 | ]; 22 | 23 | export const Serializer = { 24 | userSerializer: (user: any) => abstractSerializer(user, userFields), 25 | usersSerializer: (users: any) => { 26 | const data: any[] = []; 27 | users.forEach((user: any) => { 28 | data.push(Serializer.userSerializer(user)); 29 | }); 30 | return data; 31 | }, 32 | blogSerializer: (blog: any) => abstractSerializer(blog, blogFields), 33 | blogsSerializer: (blogs: any) => { 34 | const data: any[] = []; 35 | blogs.forEach((blog: any) => { 36 | data.push(Serializer.blogSerializer(blog)); 37 | }); 38 | return data; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/models/user.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema } from "mongoose"; 2 | import bcrypt from "bcryptjs"; 3 | 4 | const userSchema = new Schema({ 5 | name: { type: String, required: [true, "User Full Name is required"] }, 6 | email: { 7 | type: String, 8 | required: [true, "User Email is required."], 9 | unique: [true, "User Email must be unique."], 10 | trim: [true], 11 | lowecase: [true], 12 | }, 13 | password: { 14 | type: String, 15 | required: [true, "User Password is required."], 16 | min: 8, 17 | }, 18 | role: { 19 | type: String, 20 | enum: ["admin", "user"], 21 | required: true, 22 | default: "user", 23 | }, 24 | }); 25 | 26 | userSchema.pre("save", function (next) { 27 | var user = this; 28 | bcrypt.hash(user.password, 10, function (err, hash) { 29 | if (err) { 30 | return next(err); 31 | } 32 | user.password = hash; 33 | next(); 34 | }); 35 | }); 36 | 37 | const User = model("User", userSchema); 38 | 39 | userSchema.static("findUserByEmail", function (email) { 40 | return new Promise((resolve, reject) => { 41 | User.findOne({ email: email }).exec(function (err, user) { 42 | if (err) reject(err); 43 | resolve(user); 44 | }); 45 | }); 46 | }); 47 | export default User; 48 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | import express, { 5 | Express, 6 | Request, 7 | Response, 8 | ErrorRequestHandler, 9 | } from "express"; 10 | 11 | import morgan from "morgan"; 12 | import cors from "cors"; 13 | import { connectDB } from "./src/configs/db"; 14 | import apiRouter from "./src/routes/api"; 15 | // Constants 16 | const PORT = process.env.PORT || 3000; 17 | 18 | // Connect to database 19 | connectDB(); 20 | 21 | // App 22 | const app: Express = express(); 23 | app.use(cors()); 24 | // parse request bodies (req.body) 25 | app.use(express.json()); 26 | // parse requests of content-type - application/x-www-form-urlencoded 27 | app.use(express.urlencoded({ extended: true })); 28 | // Serving static assets 29 | app.use(express.static("public")); 30 | // logger 31 | app.use(morgan("combined")); 32 | 33 | // API Routes 34 | app.get("/", (req: Request, res: Response) => { 35 | res.status(200).send({ message: "Welcome to your Express App API." }); 36 | }); 37 | 38 | app.use("/api/v1/", apiRouter); 39 | 40 | /* Error handler middleware */ 41 | app.use(((err, req, res, next) => { 42 | const statusCode = err.statusCode || 500; 43 | console.error(err.message, err.stack); 44 | res.status(statusCode).json({ message: err.message }); 45 | 46 | return; 47 | }) as ErrorRequestHandler); 48 | 49 | app.listen(PORT, () => { 50 | console.log(`⚡️[server]: Server is running at https://localhost:${PORT}`); 51 | }); 52 | 53 | export default app; 54 | -------------------------------------------------------------------------------- /src/validators/user.validator.ts: -------------------------------------------------------------------------------- 1 | import { body } from "express-validator"; 2 | import User from "../models/user"; 3 | 4 | export const createUserDataValidator = [ 5 | body("name").exists().withMessage("Name is required"), 6 | body("email") 7 | .exists() 8 | // To delete leading and triling space 9 | .trim() 10 | 11 | // Normalizing the email address 12 | .normalizeEmail() 13 | .isEmail() 14 | .withMessage("Provide valid email") 15 | // Custom validation 16 | // Validate email in use or not 17 | .custom(async (email) => { 18 | const existingUser = await User.findOne({ email }); 19 | if (existingUser) { 20 | throw new Error("Email already in use"); 21 | } 22 | }), 23 | body("password") 24 | .exists() 25 | .withMessage("Password is required") 26 | .isString() 27 | .withMessage("Password should be string") 28 | .isLength({ min: 8 }) 29 | .withMessage("Password should be at least 5 characters"), 30 | ]; 31 | 32 | export const loginUserDataValidator = [ 33 | body("email").optional().isEmail().withMessage("Provide valid email"), 34 | body("password") 35 | .exists() 36 | .withMessage("Password is required") 37 | .isString() 38 | .withMessage("Password should be string"), 39 | ]; 40 | 41 | export const updateBlogDataValidator = [ 42 | body("email").isString().withMessage("Title should be string"), 43 | body("description").isString().withMessage("Description should be string"), 44 | ]; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-app-with-docker-setup", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "npx tsc", 8 | "start": "node dist/index.js", 9 | "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"", 10 | "lint": "eslint . --ext .ts", 11 | "test": "tsc && mocha dist/test/*Test.js --timeout 10000" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/bhimrazy/express-app-with-docker-setup.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/bhimrazy/express-app-with-docker-setup/issues" 22 | }, 23 | "homepage": "https://github.com/bhimrazy/express-app-with-docker-setup#readme", 24 | "dependencies": { 25 | "bcryptjs": "^2.4.3", 26 | "cors": "^2.8.5", 27 | "dotenv": "^16.0.3", 28 | "express": "^4.18.2", 29 | "express-jsdoc-swagger": "^1.8.0", 30 | "express-validator": "^6.14.2", 31 | "jsonwebtoken": "^8.5.1", 32 | "mongodb": "^4.10.0", 33 | "mongoose": "^6.6.5", 34 | "morgan": "^1.10.0" 35 | }, 36 | "devDependencies": { 37 | "@types/bcryptjs": "^2.4.2", 38 | "@types/chai": "^4.3.3", 39 | "@types/cors": "^2.8.12", 40 | "@types/express": "^4.17.14", 41 | "@types/jsonwebtoken": "^8.5.9", 42 | "@types/mocha": "^10.0.0", 43 | "@types/morgan": "^1.9.3", 44 | "@types/node": "^18.11.0", 45 | "@typescript-eslint/eslint-plugin": "^5.40.0", 46 | "@typescript-eslint/parser": "^5.40.0", 47 | "chai": "^4.3.6", 48 | "chai-http": "^4.3.0", 49 | "concurrently": "^7.4.0", 50 | "eslint": "^8.25.0", 51 | "mocha": "^10.1.0", 52 | "nodemon": "^2.0.20", 53 | "typescript": "^4.8.4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import { userService } from "../services/userService"; 2 | import { Request, Response } from "express"; 3 | import { Serializer } from "../serializers/serializers"; 4 | 5 | export const UserController = { 6 | getAllUsers: async (req: Request, res: Response) => { 7 | try { 8 | const users = await userService.getAllUsers(); 9 | res.json({ status: "success", data: Serializer.usersSerializer(users) }); 10 | } catch (err: any) { 11 | res.status(500).json({ status: "error", message: err.message }); 12 | } 13 | }, 14 | 15 | createUser: async (req: Request, res: Response) => { 16 | try { 17 | const user = await userService.createUser(req.body); 18 | res.json({ status: "success", data: user }); 19 | } catch (err: any) { 20 | res.status(500).json({ status: "error", message: err.message }); 21 | } 22 | }, 23 | 24 | getUserById: async (req: Request, res: Response) => { 25 | try { 26 | const user = await userService.getUserById(req.params.id); 27 | res.json({ status: "success", data: user }); 28 | } catch (err: any) { 29 | res.status(500).json({ status: "error", message: err.message }); 30 | } 31 | }, 32 | 33 | updateUser: async (req: Request, res: Response) => { 34 | try { 35 | const user = await userService.updateUser(req.params.id, req.body); 36 | res.json({ status: "success", data: user }); 37 | } catch (err: any) { 38 | res.status(500).json({ status: "error", message: err.message }); 39 | } 40 | }, 41 | 42 | deleteUser: async (req: Request, res: Response) => { 43 | try { 44 | const user = await userService.deleteUser(req.params.id); 45 | res.json({ status: "success", data: user }); 46 | } catch (err: any) { 47 | res.status(500).json({ status: "error", message: err.message }); 48 | } 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/controllers/blogController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { BlogService } from "../services/blogService"; 3 | import { validationResult } from "express-validator"; 4 | import { Serializer } from "../serializers/serializers"; 5 | 6 | interface userRequest extends Request { 7 | user?: any; 8 | } 9 | 10 | export const getAllBlogs = async (req: Request, res: Response) => { 11 | try { 12 | const blogs = await BlogService.getAllBlogs(); 13 | res.json({ status: "success", data: Serializer.blogsSerializer(blogs) }); 14 | } catch (err: any) { 15 | res.status(500).json({ status: "error", message: err.message }); 16 | } 17 | }; 18 | 19 | export const createBlog = async (req: userRequest, res: Response) => { 20 | try { 21 | const errors = validationResult(req); 22 | // if there is error then return Error 23 | if (!errors.isEmpty()) { 24 | return res.status(400).json({ 25 | success: false, 26 | errors: errors.array(), 27 | }); 28 | } 29 | req.body.author = req.user._id; 30 | const blog = await BlogService.createBlog(req.body); 31 | res.json({ 32 | status: "success", 33 | message: "blog created successfully.", 34 | data: Serializer.blogSerializer(blog), 35 | }); 36 | } catch (err: any) { 37 | res.status(500).json({ status: "error", message: err.message }); 38 | } 39 | }; 40 | 41 | export const getBlogById = async (req: Request, res: Response) => { 42 | try { 43 | const blog = await BlogService.getBlogById(req.params.id); 44 | res.json({ status: "success", data: Serializer.blogSerializer(blog) }); 45 | } catch (err: any) { 46 | res.status(500).json({ status: "error", message: err.message }); 47 | } 48 | }; 49 | 50 | export const updateBlog = async (req: Request, res: Response) => { 51 | try { 52 | const blog = await BlogService.updateBlog(req.params.id, req.body); 53 | res.json({ 54 | status: "success", 55 | message: "blog updated successfully.", 56 | data: Serializer.blogSerializer(blog), 57 | }); 58 | } catch (err: any) { 59 | res.status(500).json({ status: "error", message: err.message }); 60 | } 61 | }; 62 | 63 | export const deleteBlog = async (req: Request, res: Response) => { 64 | try { 65 | const blog = await BlogService.deleteBlog(req.params.id); 66 | res.json({ status: "success", message: "blog deleted successfully." }); 67 | } catch (err: any) { 68 | res.status(500).json({ status: "error", message: err.message }); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | data -------------------------------------------------------------------------------- /src/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcryptjs"; 2 | import jwt from "jsonwebtoken"; 3 | import User from "../models/user"; 4 | import { userService } from "../services/userService"; 5 | import { Request, Response } from "express"; 6 | import { validationResult } from "express-validator"; 7 | import { Serializer } from "../serializers/serializers"; 8 | 9 | interface profileRequest extends Request { 10 | user?: any; 11 | } 12 | 13 | export const AuthController = { 14 | /* register/create new user */ 15 | registerUser: async (req: Request, res: Response) => { 16 | try { 17 | const errors = validationResult(req); 18 | // if there is error then return Error 19 | if (!errors.isEmpty()) { 20 | return res.status(400).json({ 21 | status: "error", 22 | errors: errors.array(), 23 | }); 24 | } 25 | const user = req.body; 26 | if (!user.email || !user.password) { 27 | return res.status(400).send({ 28 | status: "error", 29 | message: "Username and password are required.", 30 | }); 31 | } 32 | const reg_user = await userService.createUser({ 33 | name: user.name, 34 | email: user.email, 35 | password: user.password, 36 | }); 37 | res.json({ 38 | status: "success", 39 | message: "user created successfuly", 40 | data: Serializer.userSerializer(reg_user), 41 | }); 42 | } catch (err: any) { 43 | res.status(500).json({ status: "error", message: err.message }); 44 | } 45 | }, 46 | 47 | /* user login */ 48 | loginUser: async (req: Request, res: Response) => { 49 | try { 50 | const errors = validationResult(req); 51 | 52 | // if there is error then return Error 53 | if (!errors.isEmpty()) { 54 | return res.status(400).json({ 55 | status: "error", 56 | errors: errors.array(), 57 | }); 58 | } 59 | 60 | /* check user is exist with our system */ 61 | const user = await User.findOne({ 62 | email: req.body.email, 63 | }); 64 | if (!user) { 65 | return res.status(400).send({ 66 | status: "error", 67 | message: "No account is associated with the given email", 68 | }); 69 | } 70 | 71 | /* compare password */ 72 | const isMatch = await bcrypt.compare(req.body.password, user.password); 73 | if (!isMatch) { 74 | return res 75 | .status(400) 76 | .send({ status: "error", message: "Invalid password" }); 77 | } 78 | //create token 79 | const token = jwt.sign( 80 | { _id: user._id, email: user.email, role: user.role }, 81 | process.env.JWT_SECRET!, 82 | { 83 | expiresIn: "1d", 84 | } 85 | ); 86 | res.json({ 87 | status: "success", 88 | data: { token, user: Serializer.userSerializer(user) }, 89 | }); 90 | } catch (err: any) { 91 | res.status(500).json({ status: "error", message: err.message }); 92 | } 93 | }, 94 | 95 | /* get user profile */ 96 | getUser: async (req: profileRequest, res: Response) => { 97 | const user = await User.findOne({ 98 | email: req.user.email, 99 | }); 100 | res.json({ 101 | status: "success", 102 | data: Serializer.userSerializer(user), 103 | }); 104 | }, 105 | }; 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # A Blog REST API App using ExpressJs, MongoDB, NodeJs and Typescript 6 | 7 | This repo helps you to get started with ExpressJs, MongoDB, NodeJs and Typescript in docker Environment. 8 | 9 | ## Setup and Run Locally with or without using Docker 10 | 11 | Commands 12 | 13 | ```bash 14 | # clone github repo 15 | $ git clone https://github.com/bhimrazy/express-blog-api 16 | $ cd express-blog-api 17 | $ cp .env.example .env 18 | 19 | # Run without using docker 20 | # SET DATABASE_URL 21 | $ npm install 22 | $ npm run dev 23 | 24 | # Run with docker 25 | # start containers 26 | $ docker-compose up -d 27 | # start containers 28 | $ docker-compose up -d 29 | # stop containers 30 | $ docker-compose down 31 | 32 | # check logs of docker image 33 | $ docker logs 34 | 35 | # Run tests 36 | $ npm run test 37 | ``` 38 | 39 | ## Directory Structure 40 | 41 | ``` 42 | . 43 | ├── dist/ # Build files 44 | ├── public/ # Contains static files 45 | ├── src/ # All 46 | │ ├── configs/ # Contains all the configurations 47 | │ ├── models/ # Contains all the database schema and models 48 | │ ├── services/ # Contains all the services 49 | │ ├── controllers/ # Contains all the controllers 50 | │ ├── middlewares/ # Contains all the middlewares 51 | │ ├── validators/ # Contains all the request validators 52 | │ ├── serializers/ # Contains all the serializers 53 | │ └── routes/ # Contains all the routes 54 | ├── tests/ # Contains all the test files 55 | ├── tsconfig.json # Typescript Config 56 | ├── index.ts # Index file 57 | ├── package.json 58 | ├── package-lock.json 59 | └── README.md 60 | ``` 61 | 62 | ## API Reference 63 | 64 | Postman Docs: https://documenter.getpostman.com/view/8091590/2s8YRnmXTd 65 | 66 | #### Get Home URL 67 | 68 | ``` 69 | GET /api/v1/ 70 | ``` 71 | 72 | #### Register User 73 | 74 | ``` 75 | POST /api/v1/register 76 | ``` 77 | 78 | | Parameter | Type | Description | 79 | | :--------- | :------- | :-------------------------- | 80 | | `name` | `string` | **Required**. Your Name | 81 | | `email` | `string` | **Required**. Your Email | 82 | | `password` | `string` | **Required**. Your Password | 83 | 84 | #### Login User 85 | 86 | ``` 87 | POST /api/v1/login 88 | ``` 89 | 90 | | Parameter | Type | Description | 91 | | :--------- | :------- | :-------------------------- | 92 | | `email` | `string` | **Required**. Your Email | 93 | | `password` | `string` | **Required**. Your Password | 94 | 95 | #### Blogs API 96 | 97 | ``` 98 | GET /api/v1/blogs/ 99 | GET /api/v1/blogs/:id 100 | POST /api/v1/blogs/ 101 | PUT /api/v1/blogs/:id 102 | DELETE /api/v1/blogs/:id 103 | ``` 104 | 105 | ## References 106 | 107 | ## License 108 | 109 | [MIT](https://github.com/bhimrazy/express-blog-api/blob/main/LICENSE) 110 | 111 | 112 | 131 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": { 33 | // /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // "@/*": ["./src/*"] 35 | // }, 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // "resolveJsonModule": true, /* Enable importing .json files. */ 42 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 43 | 44 | /* JavaScript Support */ 45 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 48 | 49 | /* Emit */ 50 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 51 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 55 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 56 | // "removeComments": true, /* Disable emitting comments. */ 57 | // "noEmit": true, /* Disable emitting files from a compilation. */ 58 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 59 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 64 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 65 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 66 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 67 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 68 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 69 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 70 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 71 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 72 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 80 | 81 | /* Type Checking */ 82 | "strict": true /* Enable all strict type-checking options. */, 83 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 84 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 86 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 88 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 89 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | 102 | /* Completeness */ 103 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 105 | } 106 | } 107 | --------------------------------------------------------------------------------