├── .gitignore ├── LICENSE ├── README.md ├── db └── index.js ├── docs └── swagger.json ├── env ├── index.js ├── package.json ├── routes ├── private │ ├── createPost.js │ ├── deletePost.js │ ├── getMe.js │ ├── index.js │ └── updatePost.js └── public │ ├── getPosts.js │ ├── index.js │ ├── signin.js │ └── signup.js ├── swagger.js └── validation ├── createPost.js ├── signin.js ├── signup.js └── updatePost.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env 4 | .env.local -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tuhin Kanti Pal 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express MongoDB Workflow 2 | 3 | ### You should follow this workflow to build your easily maintainable, secure API with ExpressJS and MongoDB. 4 | 5 | Check source code you will get the idea. This workflow is also followed by me. Create an issue to give me suggestions. Don't forget to read the docs (Perfectly autogenerated with swagger-autogen), it is available on `http://localhost:3000/docs`. 6 | 7 | ### Setup DEV enviroment 8 | 9 | - Rename `env` to `.env` & fill everything 10 | - Install dependencies by running `npm i` 11 | - `npm run dev` to run this as development 12 | 13 | ### Advance Note: 14 | 15 | - Create a fulltext search index `db.posts.createIndex({ title: "text", content: "text" })` 16 | 17 | ### License & Copyright : 18 | 19 | - This Project is [MIT](https://github.com/cachecleanerjeet/express-mongodb-workflow/blob/master/LICENSE) Licensed 20 | - Copyright 2021 by [Tuhin Kanti Pal](https://github.com/cachecleanerjeet) 21 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require("mongodb"); 2 | require("dotenv").config(); 3 | 4 | module.exports = async (database) => { 5 | var conn = await MongoClient.connect(process.env.MONGO_URL, { 6 | useNewUrlParser: true, 7 | useUnifiedTopology: true, 8 | }); 9 | return { 10 | conn, 11 | db: conn.db(database || process.env.DATABASE_NAME), 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Express MongoDB Workflow", 5 | "description": "You should follow this workflow to build your easily maintainable, secure API with ExpressJS and MongoDB.", 6 | "version": "1.0.0" 7 | }, 8 | "host": "localhost:3000", 9 | "basePath": "/", 10 | "tags": [], 11 | "schemes": [ 12 | "https", 13 | "http" 14 | ], 15 | "securityDefinitions": { 16 | "apiKeyAuth": { 17 | "type": "apiKey", 18 | "in": "header", 19 | "name": "authorization", 20 | "description": "Your jwt session token. You can retrive it by signin or signup" 21 | } 22 | }, 23 | "consumes": [], 24 | "produces": [], 25 | "paths": { 26 | "/public/signup": { 27 | "post": { 28 | "tags": [], 29 | "description": "", 30 | "parameters": [ 31 | { 32 | "name": "obj", 33 | "in": "body", 34 | "schema": { 35 | "type": "object", 36 | "properties": { 37 | "email": { 38 | "example": "any" 39 | }, 40 | "password": { 41 | "example": "any" 42 | }, 43 | "phone": { 44 | "example": "any" 45 | }, 46 | "name": { 47 | "example": "any" 48 | }, 49 | "address": { 50 | "example": "any" 51 | }, 52 | "city": { 53 | "example": "any" 54 | }, 55 | "state": { 56 | "example": "any" 57 | }, 58 | "zipcode": { 59 | "example": "any" 60 | }, 61 | "country": { 62 | "example": "any" 63 | } 64 | } 65 | } 66 | } 67 | ], 68 | "responses": { 69 | "201": { 70 | "description": "Created" 71 | }, 72 | "400": { 73 | "description": "Bad Request" 74 | }, 75 | "409": { 76 | "description": "Conflict" 77 | }, 78 | "500": { 79 | "description": "Internal Server Error" 80 | } 81 | } 82 | } 83 | }, 84 | "/public/signin": { 85 | "post": { 86 | "tags": [], 87 | "description": "", 88 | "parameters": [ 89 | { 90 | "name": "obj", 91 | "in": "body", 92 | "schema": { 93 | "type": "object", 94 | "properties": { 95 | "email": { 96 | "example": "any" 97 | }, 98 | "password": { 99 | "example": "any" 100 | } 101 | } 102 | } 103 | } 104 | ], 105 | "responses": { 106 | "200": { 107 | "description": "OK" 108 | }, 109 | "400": { 110 | "description": "Bad Request" 111 | }, 112 | "401": { 113 | "description": "Unauthorized" 114 | }, 115 | "404": { 116 | "description": "Not Found" 117 | }, 118 | "500": { 119 | "description": "Internal Server Error" 120 | } 121 | } 122 | } 123 | }, 124 | "/public/getPosts": { 125 | "post": { 126 | "tags": [], 127 | "description": "", 128 | "parameters": [ 129 | { 130 | "name": "limit", 131 | "in": "query", 132 | "type": "string" 133 | }, 134 | { 135 | "name": "page", 136 | "in": "query", 137 | "type": "string" 138 | }, 139 | { 140 | "name": "category", 141 | "in": "query", 142 | "type": "string" 143 | }, 144 | { 145 | "name": "tags", 146 | "in": "query", 147 | "type": "string" 148 | }, 149 | { 150 | "name": "searchQuery", 151 | "in": "query", 152 | "type": "string" 153 | }, 154 | { 155 | "name": "sortByDate", 156 | "in": "query", 157 | "type": "string" 158 | } 159 | ], 160 | "responses": { 161 | "200": { 162 | "description": "OK" 163 | }, 164 | "500": { 165 | "description": "Internal Server Error" 166 | } 167 | } 168 | } 169 | }, 170 | "/private/getMe": { 171 | "get": { 172 | "tags": [], 173 | "description": "", 174 | "parameters": [], 175 | "responses": { 176 | "200": { 177 | "description": "OK" 178 | }, 179 | "401": { 180 | "description": "Unauthorized" 181 | }, 182 | "500": { 183 | "description": "Internal Server Error" 184 | } 185 | }, 186 | "security": [ 187 | { 188 | "apiKeyAuth": [] 189 | } 190 | ] 191 | } 192 | }, 193 | "/private\r/createPost": { 194 | "post": { 195 | "tags": [], 196 | "description": "", 197 | "parameters": [ 198 | { 199 | "name": "obj", 200 | "in": "body", 201 | "schema": { 202 | "type": "object", 203 | "properties": { 204 | "title": { 205 | "example": "any" 206 | }, 207 | "content": { 208 | "example": "any" 209 | }, 210 | "isPrivate": { 211 | "example": "any" 212 | }, 213 | "tags": { 214 | "example": "any" 215 | }, 216 | "images": { 217 | "example": "any" 218 | }, 219 | "category": { 220 | "example": "any" 221 | } 222 | } 223 | } 224 | } 225 | ], 226 | "responses": { 227 | "201": { 228 | "description": "Created" 229 | }, 230 | "400": { 231 | "description": "Bad Request" 232 | }, 233 | "401": { 234 | "description": "Unauthorized" 235 | }, 236 | "409": { 237 | "description": "Conflict" 238 | }, 239 | "500": { 240 | "description": "Internal Server Error" 241 | } 242 | }, 243 | "security": [ 244 | { 245 | "apiKeyAuth": [] 246 | } 247 | ] 248 | } 249 | }, 250 | "/private\r/updatePost/{postId}": { 251 | "post": { 252 | "tags": [], 253 | "description": "", 254 | "parameters": [ 255 | { 256 | "name": "postId", 257 | "in": "path", 258 | "required": true, 259 | "type": "string" 260 | }, 261 | { 262 | "name": "obj", 263 | "in": "body", 264 | "schema": { 265 | "type": "object", 266 | "properties": { 267 | "title": { 268 | "example": "any" 269 | }, 270 | "content": { 271 | "example": "any" 272 | }, 273 | "isPrivate": { 274 | "example": "any" 275 | }, 276 | "tags": { 277 | "example": "any" 278 | }, 279 | "images": { 280 | "example": "any" 281 | }, 282 | "category": { 283 | "example": "any" 284 | } 285 | } 286 | } 287 | } 288 | ], 289 | "responses": { 290 | "201": { 291 | "description": "Created" 292 | }, 293 | "400": { 294 | "description": "Bad Request" 295 | }, 296 | "401": { 297 | "description": "Unauthorized" 298 | }, 299 | "404": { 300 | "description": "Not Found" 301 | }, 302 | "500": { 303 | "description": "Internal Server Error" 304 | } 305 | }, 306 | "security": [ 307 | { 308 | "apiKeyAuth": [] 309 | } 310 | ] 311 | } 312 | }, 313 | "/private/deletePost/{postId}": { 314 | "delete": { 315 | "tags": [], 316 | "description": "", 317 | "parameters": [ 318 | { 319 | "name": "postId", 320 | "in": "path", 321 | "required": true, 322 | "type": "string" 323 | } 324 | ], 325 | "responses": { 326 | "200": { 327 | "description": "OK" 328 | }, 329 | "401": { 330 | "description": "Unauthorized" 331 | }, 332 | "404": { 333 | "description": "Not Found" 334 | }, 335 | "500": { 336 | "description": "Internal Server Error" 337 | } 338 | }, 339 | "security": [ 340 | { 341 | "apiKeyAuth": [] 342 | } 343 | ] 344 | } 345 | } 346 | }, 347 | "definitions": {} 348 | } -------------------------------------------------------------------------------- /env: -------------------------------------------------------------------------------- 1 | JWT_SECRET="JWT_SECRET" 2 | MONGO_URL="YOUR_MONGO_URL" 3 | DATABASE_NAME="DATABASE_NAME" 4 | APPLICATION_HOST="localhost:3000" -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const swaggerUi = require("swagger-ui-express"); 4 | 5 | app.use(express.json()); 6 | app.use(express.urlencoded({ extended: true })); 7 | app.use(require("cors")()); 8 | 9 | app.use( 10 | "/docs", 11 | swaggerUi.serve, 12 | swaggerUi.setup(require("./docs/swagger.json")) 13 | ); // swagger-ui-express 14 | app.use("/public", require("./routes/public")); // Public routes 15 | app.use("/private", require("./routes/private")); // Private routes 16 | 17 | const port = process.env.PORT || 3000; 18 | app.listen(port, () => { 19 | console.log(`🚀 Server is running on port ${port}`); 20 | }); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-mongodb-workflow", 3 | "version": "1.0.0", 4 | "description": "You should follow this workflow to build your easily maintainable, secure API with ExpressJS and MongoDB.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon index.js", 8 | "start": "npm run swagger && node index.js", 9 | "swagger": "node swagger.js" 10 | }, 11 | "keywords": [], 12 | "homepage": "https://github.com/cachecleanerjeet/express-mongodb-workflow#readme", 13 | "author": "cachecleanerjeet ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "bcrypt": "^5.0.1", 17 | "cors": "^2.8.5", 18 | "dotenv": "^10.0.0", 19 | "express": "^4.17.1", 20 | "jsonwebtoken": "^8.5.1", 21 | "mongodb": "^4.1.1", 22 | "swagger-autogen": "^2.11.2", 23 | "swagger-ui-express": "^4.1.6", 24 | "validator": "^13.6.0", 25 | "xss": "^1.0.9" 26 | }, 27 | "devDependencies": { 28 | "nodemon": "^2.0.12" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/cachecleanerjeet/express-mongodb-workflow.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/cachecleanerjeet/express-mongodb-workflow/issues" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /routes/private/createPost.js: -------------------------------------------------------------------------------- 1 | const database = require("../../db"); 2 | 3 | module.exports = async (req, res) => { 4 | try { 5 | var { conn, db } = await database(); 6 | const { id } = req.user; // we get the user id from the token payload it is verified by the middleware 7 | 8 | // check if same Title exists 9 | var checkIfSameTitleExists = await db.collection("posts").findOne({ 10 | userId: id, 11 | title: req.body.title, 12 | }); 13 | 14 | if (checkIfSameTitleExists) { 15 | res.status(409).json({ 16 | status: false, 17 | message: "Same title exists", 18 | reason: "You already have a post with the same title", 19 | }); 20 | } else { 21 | var create = await db.collection("posts").insertOne({ 22 | // we create a new post 23 | ...req.body, // already sanitized by middleware 24 | userId: id, 25 | createdAt: new Date(), 26 | updatedAt: new Date(), 27 | }); 28 | 29 | if (create.acknowledged) { 30 | res.status(201).json({ 31 | status: true, 32 | message: "Post created", 33 | data: { 34 | postId: create.insertedId, 35 | postContent: req.body, 36 | }, 37 | }); 38 | } else { 39 | throw new Error("Post not created"); 40 | } 41 | } 42 | } catch (error) { 43 | res.status(500).json({ 44 | status: false, 45 | message: "Something went wrong", 46 | reason: error.toString(), 47 | }); 48 | } finally { 49 | if (conn) { 50 | // important to close the connection 51 | await conn.close(); 52 | } 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /routes/private/deletePost.js: -------------------------------------------------------------------------------- 1 | const { ObjectId } = require("mongodb"); 2 | const database = require("../../db"); 3 | 4 | module.exports = async (req, res) => { 5 | try { 6 | var { conn, db } = await database(); 7 | const { id } = req.user; // we get the user id from the token payload it is verified by the middleware 8 | 9 | // check if post exists 10 | var checkIfExists = await db.collection("posts").findOne({ 11 | userId: id, 12 | _id: ObjectId(req.params.postId), // we get the post id from the url, 13 | }); 14 | 15 | if (checkIfExists) { 16 | var deletePost = await db.collection("posts").deleteOne({ 17 | userId: id, 18 | _id: ObjectId(req.params.postId), 19 | }); 20 | 21 | if (deletePost.acknowledged) { 22 | res.status(200).json({ 23 | status: true, 24 | message: "Post deleted", 25 | data: { 26 | postId: req.params.postId, 27 | }, 28 | }); 29 | } else { 30 | throw new Error("Post not deleted"); 31 | } 32 | } else { 33 | res.status(404).json({ 34 | status: false, 35 | message: "Post not found", 36 | reason: "This post is not exists. Please create a post first !", 37 | }); 38 | } 39 | } catch (error) { 40 | res.status(500).json({ 41 | status: false, 42 | message: "Something went wrong", 43 | reason: error.toString(), 44 | }); 45 | } finally { 46 | if (conn) { 47 | // important to close the connection 48 | await conn.close(); 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /routes/private/getMe.js: -------------------------------------------------------------------------------- 1 | const { ObjectId } = require("mongodb"); 2 | const database = require("../../db"); 3 | 4 | module.exports = async (req, res) => { 5 | try { 6 | var { conn, db } = await database(); 7 | const { id } = req.user; // we get the user id from the token payload it is verified by the middleware 8 | 9 | const user = await db.collection("users").findOne({ _id: ObjectId(id) }); 10 | if (user) { 11 | delete user.password; 12 | res.status(200).json({ 13 | status: true, 14 | message: "Profile fetched successfully", 15 | data: user, 16 | }); 17 | } else { 18 | throw new Error("User not found"); 19 | } 20 | } catch (error) { 21 | res.status(500).json({ 22 | status: false, 23 | message: "Something went wrong", 24 | reason: error.toString(), 25 | }); 26 | } finally { 27 | if (conn) { 28 | // important to close the connection 29 | await conn.close(); 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /routes/private/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const jwt = require("jsonwebtoken"); 3 | const router = express.Router(); 4 | require("dotenv").config(); 5 | 6 | function isAuthenticated(req, res, next) { 7 | /* #swagger.security = [{ 8 | "apiKeyAuth": [] 9 | }] */ 10 | 11 | try { 12 | const user = jwt.verify( 13 | req.headers.authorization, // accept token from header(authorization) 14 | process.env.JWT_SECRET 15 | ); 16 | req.user = user; 17 | return next(); 18 | } catch (error) { 19 | // means jwt expired 20 | return res.status(401).json({ 21 | status: false, 22 | message: "Unauthorized", 23 | reason: error.toString(), 24 | }); 25 | } 26 | } 27 | 28 | router.get("/getMe", isAuthenticated, require("./getMe")); 29 | 30 | router.post( 31 | "/createPost", 32 | [isAuthenticated, require("../../validation/createPost")], 33 | require("./createPost") 34 | ); 35 | 36 | router.post( 37 | "/updatePost/:postId", 38 | [isAuthenticated, require("../../validation/updatePost")], 39 | require("./updatePost") 40 | ); 41 | 42 | router.delete("/deletePost/:postId", isAuthenticated, require("./deletePost")); 43 | 44 | module.exports = router; 45 | -------------------------------------------------------------------------------- /routes/private/updatePost.js: -------------------------------------------------------------------------------- 1 | const { ObjectId } = require("mongodb"); 2 | const database = require("../../db"); 3 | 4 | module.exports = async (req, res) => { 5 | try { 6 | var { conn, db } = await database(); 7 | const { id } = req.user; // we get the user id from the token payload it is verified by the middleware 8 | 9 | // check if post exists 10 | var checkIfExists = await db.collection("posts").findOne({ 11 | userId: id, 12 | _id: ObjectId(req.params.postId), // we get the post id from the url, 13 | }); 14 | 15 | if (checkIfExists) { 16 | var updatePost = await db.collection("posts").updateOne( 17 | { 18 | userId: id, 19 | _id: ObjectId(req.params.postId), 20 | }, 21 | { 22 | $set: { 23 | ...req.body, 24 | updatedAt: new Date(), 25 | }, 26 | } 27 | ); 28 | 29 | if (updatePost.acknowledged) { 30 | res.status(201).json({ 31 | status: true, 32 | message: "Post updated", 33 | data: { 34 | postId: req.params.postId, 35 | updatedItems: req.body, 36 | }, 37 | }); 38 | } else { 39 | throw new Error("Post not updated"); 40 | } 41 | } else { 42 | res.status(404).json({ 43 | status: false, 44 | message: "Post not found", 45 | reason: "This post is not exists. Please create a post first !", 46 | }); 47 | } 48 | } catch (error) { 49 | res.status(500).json({ 50 | status: false, 51 | message: "Something went wrong", 52 | reason: error.toString(), 53 | }); 54 | } finally { 55 | if (conn) { 56 | // important to close the connection 57 | await conn.close(); 58 | } 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /routes/public/getPosts.js: -------------------------------------------------------------------------------- 1 | const database = require("../../db"); 2 | 3 | module.exports = async (req, res) => { 4 | try { 5 | var { conn, db } = await database(); 6 | 7 | var findSchema = {}; 8 | var sortSchema = {}; 9 | var limit = 24; 10 | var skip = 0; 11 | 12 | if (req.query.limit && !isNaN(Number(req.query.limit))) { 13 | // calculate limit 14 | limit = Number(req.query.limit); 15 | } 16 | 17 | if (req.query.page && !isNaN(req.query.page)) { 18 | // calculate skip 19 | skip = (Number(req.query.page) - 1) * limit; 20 | } 21 | 22 | if (req.query.category) { 23 | // find by category 24 | if (req.query.category.includes(",")) { 25 | findSchema["category"] = { $in: req.query.category.split(",") }; 26 | } else { 27 | findSchema["category"] = { $in: [req.query.category] }; 28 | } 29 | } 30 | 31 | if (req.query.tags) { 32 | // find by tags 33 | if (req.query.tags.includes(",")) { 34 | findSchema["tags"] = { $in: req.query.tags.split(",") }; 35 | } else { 36 | findSchema["tags"] = { $in: [req.query.tags] }; 37 | } 38 | } 39 | 40 | if (req.query.searchQuery) { 41 | // find by searchQuery 42 | findSchema["$text"] = { $search: req.query.searchQuery }; 43 | } else { 44 | // sort by date 45 | if (req.query.sortByDate === "old") { 46 | sortSchema["createdAt"] = 1; 47 | } else { 48 | sortSchema["createdAt"] = -1; 49 | } 50 | } 51 | 52 | var getPosts = await db 53 | .collection("posts") 54 | .find({ ...findSchema, isPrivate: false }) 55 | .sort(sortSchema) 56 | .limit(limit) 57 | .skip(skip) 58 | .project({ 59 | _id: 1, 60 | title: 1, 61 | content: 1, 62 | images: 1, 63 | category: 1, 64 | tags: 1, 65 | createdAt: 1, 66 | updatedAt: 1, 67 | }) 68 | .toArray(); 69 | 70 | var totalCount = await db 71 | .collection("posts") 72 | .countDocuments({ ...findSchema, isPrivate: false }); // get total count 73 | 74 | if (totalCount > getPosts.length + skip) { 75 | // if there are more posts 76 | var nextPage = { 77 | has: true, 78 | pageNo: !isNaN(Number(req.query.page)) ? Number(req.query.page) + 1 : 2, 79 | }; 80 | } else { 81 | // if there are no more posts 82 | var nextPage = { 83 | has: false, 84 | pageNo: 0, 85 | }; 86 | } 87 | 88 | if (skip > 0) { 89 | // if there are previous posts 90 | var prevPage = { 91 | has: true, 92 | pageNo: !isNaN(Number(req.query.page)) ? Number(req.query.page) - 1 : 1, 93 | }; 94 | } else { 95 | // if there are no previous posts 96 | var prevPage = { 97 | has: false, 98 | pageNo: 0, 99 | }; 100 | } 101 | 102 | res.status(200).json({ 103 | status: true, 104 | message: "Posts fetched successfully", 105 | data: { 106 | totalCount, 107 | posts: getPosts, 108 | prevPage, 109 | nextPage, 110 | }, 111 | }); 112 | } catch (error) { 113 | res.status(500).json({ 114 | status: false, 115 | message: "Something went wrong", 116 | reason: error.toString(), 117 | }); 118 | } finally { 119 | if (conn) { 120 | // important to close the connection 121 | await conn.close(); 122 | } 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /routes/public/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | router.post("/signup", require("../../validation/signup"), require("./signup")); 5 | router.post("/signin", require("../../validation/signin"), require("./signin")); 6 | router.get("/getPosts", require("./getPosts")); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /routes/public/signin.js: -------------------------------------------------------------------------------- 1 | const database = require("../../db"); 2 | const bcrypt = require("bcrypt"); 3 | const jwt = require("jsonwebtoken"); 4 | 5 | module.exports = async (req, res) => { 6 | try { 7 | var { conn, db } = await database(); 8 | 9 | var findUser = await db.collection("users").findOne({ 10 | email: req.body.email, 11 | }); // check if user already exists 12 | 13 | if (findUser) { 14 | var password = bcrypt.compareSync(req.body.password, findUser.password); 15 | if (password) { 16 | // sign jwt 17 | var token = jwt.sign( 18 | { 19 | exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24, 20 | email: findUser.email, 21 | id: findUser._id, 22 | }, 23 | process.env.JWT_SECRET 24 | ); 25 | 26 | res.status(200).json({ 27 | status: true, 28 | message: "User logged in successfully", 29 | token, 30 | }); 31 | } else { 32 | res.status(401).json({ 33 | status: false, 34 | message: "Wrong password", 35 | reason: "Wrong password provided", 36 | }); 37 | } 38 | } else { 39 | res.status(404).json({ 40 | status: false, 41 | message: "Not found, Please sign up !", 42 | reason: "User not found", 43 | }); 44 | } 45 | } catch (error) { 46 | res.status(500).json({ 47 | status: false, 48 | message: "Something went wrong", 49 | reason: error.toString(), 50 | }); 51 | } finally { 52 | if (conn) { 53 | // closing connection is important 54 | await conn.close(); 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /routes/public/signup.js: -------------------------------------------------------------------------------- 1 | const database = require("../../db"); 2 | const bcrypt = require("bcrypt"); 3 | const jwt = require("jsonwebtoken"); 4 | 5 | module.exports = async (req, res) => { 6 | try { 7 | var { conn, db } = await database(); 8 | 9 | var checkIfExists = await db.collection("users").findOne({ 10 | email: req.body.email, 11 | }); // check if user already exists 12 | 13 | if (checkIfExists) { 14 | res.status(409).send({ 15 | status: false, 16 | message: "Already exists, please sign in !", 17 | reason: "User already exists", 18 | }); 19 | } else { 20 | // create user 21 | var createUser = await db.collection("users").insertOne({ 22 | ...req.body, //already sanitized 23 | password: bcrypt.hashSync(req.body.password, 10), // hashing is important 24 | updatedAt: new Date(), 25 | createdAt: new Date(), 26 | }); 27 | 28 | if (createUser.acknowledged) { 29 | // sign jwt 30 | var token = jwt.sign( 31 | { 32 | exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24, 33 | email: req.body.email, 34 | id: createUser.insertedId, 35 | }, 36 | process.env.JWT_SECRET 37 | ); 38 | 39 | res.status(201).send({ 40 | status: true, 41 | message: "User created successfully", 42 | token, 43 | }); 44 | } else { 45 | throw new Error("Error creating user"); 46 | } 47 | } 48 | } catch (error) { 49 | res.status(500).json({ 50 | status: false, 51 | message: "Something went wrong", 52 | reason: error.toString(), 53 | }); 54 | } finally { 55 | if (conn) { 56 | await conn.close(); 57 | } 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /swagger.js: -------------------------------------------------------------------------------- 1 | const swaggerAutogen = require("swagger-autogen")(); 2 | require("dotenv").config(); 3 | 4 | const outputFile = "./docs/swagger.json"; 5 | const endpointsFiles = ["./index.js"]; 6 | 7 | swaggerAutogen(outputFile, endpointsFiles, { 8 | info: { 9 | title: "Express MongoDB Workflow", 10 | description: 11 | "You should follow this workflow to build your easily maintainable, secure API with ExpressJS and MongoDB.", 12 | }, 13 | securityDefinitions: { 14 | apiKeyAuth: { 15 | type: "apiKey", 16 | in: "header", 17 | name: "authorization", 18 | description: 19 | "Your jwt session token. You can retrive it by signin or signup", 20 | }, 21 | }, 22 | host: process.env.APPLICATION_HOST || "localhost:3000", 23 | schemes: ["https", "http"], 24 | }); 25 | -------------------------------------------------------------------------------- /validation/createPost.js: -------------------------------------------------------------------------------- 1 | const validator = require("validator"); 2 | const xss = require("xss"); 3 | 4 | 5 | // never trust user input 6 | 7 | // body : title, content, isPrivate, tags(array), images(array), category(array) 8 | 9 | module.exports = (req, res, next) => { 10 | try { 11 | if (!validator.isLength(req.body.title, { min: 2, max: undefined })) throw new Error("Title must be at least 2 characters"); 12 | if (!validator.isLength(req.body.content, { min: 100, max: undefined })) throw new Error("Content must be at least 100 characters"); 13 | if (typeof req.body.isPrivate !=="boolean") throw new Error("isPrivate must be a boolean"); 14 | if (!Array.isArray(req.body.tags)) throw new Error("Tags must be an array"); 15 | if (!Array.isArray(req.body.images)) throw new Error("Images must be an array"); 16 | if (!Array.isArray(req.body.category)) throw new Error("Category must be an array"); 17 | 18 | for (var i = 0; i < req.body.tags.length; i++) { 19 | if (typeof req.body.tags[i] !== "string") throw new Error(`Tag item must be a string (Error in tag no ${i})`); 20 | } 21 | for (var i = 0; i < req.body.images.length; i++) { 22 | if (typeof req.body.images[i] !== "string") throw new Error(`Image item must be a string (Error in images no ${i})`); 23 | } 24 | for (var i = 0; i < req.body.category.length; i++) { 25 | if (typeof req.body.category[i] !== "string") throw new Error(`Category item must be a string (Error in category no ${i})`); 26 | } 27 | 28 | // html sanitization 29 | req.body.title = xss(req.body.title); 30 | req.body.content = xss(req.body.content); 31 | 32 | 33 | next(); 34 | } catch (error) { 35 | res.status(400).json({ 36 | status: false, 37 | message: "Bad Request", 38 | reason: error.toString(), 39 | }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /validation/signin.js: -------------------------------------------------------------------------------- 1 | const validator = require("validator"); 2 | 3 | // never trust user input 4 | 5 | // body : email, password 6 | 7 | module.exports = (req, res, next) => { 8 | try { 9 | if (!validator.isEmail(req.body.email)) throw new Error("Invalid email"); 10 | if (!validator.isLength(req.body.password, { min: 6, max: undefined })) throw new Error("Password must be at least 6 characters"); 11 | next(); 12 | } catch (error) { 13 | res.status(400).json({ 14 | status: false, 15 | message: "Bad Request", 16 | reason: error.toString(), 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /validation/signup.js: -------------------------------------------------------------------------------- 1 | const validator = require("validator"); 2 | 3 | // never trust user input 4 | 5 | // body : email, password, phone number, name optional: address, city, state, zipcode, country, 6 | 7 | module.exports = (req, res, next) => { 8 | try { 9 | if (!validator.isEmail(req.body.email)) throw new Error("Invalid email"); 10 | if (!validator.isLength(req.body.password, { min: 6, max: undefined })) throw new Error("Password must be at least 6 characters"); 11 | if (!validator.isMobilePhone(req.body.phone, "any")) throw new Error("Invalid phone number"); 12 | if (!validator.isLength(req.body.name, { min: 2, max: undefined })) throw new Error("Name must be at least 2 characters"); 13 | if (req.body.address && !validator.isLength(req.body.address, { min: 2, max: undefined })) throw new Error("Address must be at least 2 characters"); 14 | if (req.body.city && !validator.isLength(req.body.city, { min: 2, max: undefined })) throw new Error("City must be at least 2 characters"); 15 | if (req.body.state && !validator.isLength(req.body.state, { min: 2, max: undefined })) throw new Error("State must be at least 2 characters"); 16 | if (req.body.zipcode && !validator.isLength(req.body.zipcode, { min: 5, max: undefined })) throw new Error("Zipcode must be at least 5 characters"); 17 | if (req.body.country && !validator.isLength(req.body.country, { min: 2, max: undefined })) throw new Error("Country must be at least 2 characters"); 18 | 19 | 20 | next(); 21 | } catch (error) { 22 | res.status(400).json({ 23 | status: false, 24 | message: "Bad Request", 25 | reason: error.toString(), 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /validation/updatePost.js: -------------------------------------------------------------------------------- 1 | const validator = require("validator"); 2 | const xss = require("xss"); 3 | 4 | 5 | // never trust user input 6 | 7 | // body : title, content, isPrivate, tags(array), images(array), category(array) 8 | 9 | module.exports = (req, res, next) => { 10 | try { 11 | if (req.body.title && !validator.isLength(req.body.title, { min: 2, max: undefined })) throw new Error("Title must be at least 2 characters"); 12 | if (req.body.content && !validator.isLength(req.body.content, { min: 100, max: undefined })) throw new Error("Content must be at least 100 characters"); 13 | if (req.body.isPrivate && typeof req.body.isPrivate !== "boolean") throw new Error("isPrivate must be a boolean"); 14 | if (req.body.tags && !Array.isArray(req.body.tags)) throw new Error("Tags must be an array"); 15 | if (req.body.images && !Array.isArray(req.body.images)) throw new Error("Image must be an array"); 16 | if (req.body.category && !Array.isArray(req.body.category)) throw new Error("Category must be an array"); 17 | 18 | if (req.body.tags) { 19 | for (var i = 0; i < req.body.tags.length; i++) { 20 | if (typeof req.body.tags[i] !== "string") throw new Error(`Tag item must be a string (Error in tag no ${i})`); 21 | } 22 | } 23 | 24 | if (req.body.images) { 25 | for (var i = 0; i < req.body.images.length; i++) { 26 | if (typeof req.body.images[i] !== "string") throw new Error(`Image item must be a string (Error in images no ${i})`); 27 | } 28 | } 29 | 30 | if (req.body.category) { 31 | for (var i = 0; i < req.body.category.length; i++) { 32 | if (typeof req.body.category[i] !== "string") throw new Error(`Category item must be a string (Error in category no ${i})`); 33 | } 34 | } 35 | 36 | // html sanitization 37 | if (req.body.title) req.body.title = xss(req.body.title); 38 | if (req.body.content) req.body.content = xss(req.body.content); 39 | 40 | var allBodyItemsKeys = Object.keys(req.body) // remove undefined empty items 41 | 42 | for (var i = 0; i < allBodyItemsKeys.length; i++) { 43 | if (req.body[allBodyItemsKeys[i]] === undefined || req.body[allBodyItemsKeys[i]] === "") delete req.body[allBodyItemsKeys[i]]; 44 | } 45 | 46 | next(); 47 | } catch (error) { 48 | res.status(400).json({ 49 | status: false, 50 | message: "Bad Request", 51 | reason: error.toString(), 52 | }); 53 | } 54 | }; 55 | --------------------------------------------------------------------------------