├── .env.sample ├── src ├── index.js ├── api │ ├── payment.js │ ├── index.js │ ├── fibonacci.js │ ├── login.js │ └── register.js ├── models │ └── user.js ├── database │ └── index.js ├── middlewares.js ├── auth │ └── passport.js ├── app.js └── pages │ └── homepage.html ├── .eslintrc.js ├── test ├── app.test.js └── api.test.js ├── benchmarks ├── basic-get.js └── login-register.js ├── LICENSE ├── package.json ├── .gitignore └── README.md /.env.sample: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | DB_NAME=name 3 | DB_USER=user 4 | DB_PASS=pass -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const app = require("./app"); 2 | require("dotenv").config(); 3 | 4 | const port = process.env.PORT || 5000; 5 | app.listen(port, () => { 6 | console.log(`Listening: http://localhost:${port}`); 7 | }); 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | }, 5 | extends: 'airbnb-base', 6 | rules: { 7 | 'comma-dangle': 0, 8 | 'no-underscore-dangle': 0, 9 | 'no-param-reassign': 0, 10 | 'no-return-assign': 0, 11 | camelcase: 0, 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/payment.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const passport = require("passport"); 3 | 4 | const router = express.Router(); 5 | 6 | router.get( 7 | "/payment", 8 | passport.authenticate("jwt", { session: false }), 9 | (req, res) => { 10 | res.send("You have a total of: 2400$"); 11 | } 12 | ); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const registerApi = require("./register"); 3 | const loginApi = require("./login"); 4 | const paymentApi = require("./payment"); 5 | const fibonacciApi = require("./fibonacci"); 6 | 7 | const router = express.Router(); 8 | 9 | router.use(registerApi); 10 | router.use(loginApi); 11 | router.use(fibonacciApi); 12 | router.use(paymentApi); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const sequelize = require("../database"); 3 | 4 | const User = sequelize.define("User", { 5 | fullName: { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | }, 9 | email: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | password: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | }, 17 | }); 18 | 19 | module.exports = User; 20 | -------------------------------------------------------------------------------- /src/database/index.js: -------------------------------------------------------------------------------- 1 | const { Sequelize } = require("sequelize"); 2 | 3 | const sequelize = new Sequelize( 4 | process.env.DB_NAME, 5 | process.env.DB_USER, 6 | process.env.DB_PASS, 7 | { 8 | host: "localhost", 9 | dialect: "mysql", 10 | } 11 | ); 12 | 13 | sequelize.sync(); 14 | 15 | (async () => { 16 | try { 17 | await sequelize.authenticate(); 18 | console.log("Connection has been established successfully."); 19 | } catch (error) { 20 | console.error("Unable to connect to the database:", error); 21 | } 22 | })(); 23 | 24 | module.exports = sequelize; 25 | -------------------------------------------------------------------------------- /src/middlewares.js: -------------------------------------------------------------------------------- 1 | function notFound(req, res, next) { 2 | res.status(404); 3 | const error = new Error(`🔍 - Not Found - ${req.originalUrl}`); 4 | next(error); 5 | } 6 | 7 | /* eslint-disable no-unused-vars */ 8 | function errorHandler(err, req, res, next) { 9 | /* eslint-enable no-unused-vars */ 10 | console.log("Error: ", err && err.message); 11 | const statusCode = res.statusCode !== 200 ? res.statusCode : 500; 12 | res.status(statusCode); 13 | res.json({ 14 | message: err.message, 15 | stack: process.env.NODE_ENV === "production" ? "🥞" : err.stack, 16 | }); 17 | } 18 | 19 | module.exports = { 20 | notFound, 21 | errorHandler, 22 | }; 23 | -------------------------------------------------------------------------------- /src/api/fibonacci.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const router = express.Router(); 4 | 5 | function fib(n) { 6 | if (n > 1) { 7 | return fib(n - 1) + fib(n - 2); 8 | } else { 9 | return n; 10 | } 11 | } 12 | 13 | router.get("/boom", (req, res) => { 14 | res.send("Hola"); 15 | }); 16 | 17 | router.get("/calculate/:num", (req, res) => { 18 | const num = parseInt(req.params.num); 19 | 20 | if (typeof num !== "number" || num === NaN) 21 | return res 22 | .status(400) 23 | .json({ message: "Please provide valid Integer for calculation" }); 24 | 25 | const f = fib(num); 26 | 27 | res.json({ answer: f }); 28 | }); 29 | 30 | module.exports = router; 31 | -------------------------------------------------------------------------------- /test/app.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const app = require('../src/app'); 4 | 5 | describe('app', () => { 6 | it('responds with a not found message', (done) => { 7 | request(app) 8 | .get('/what-is-this-even') 9 | .set('Accept', 'application/json') 10 | .expect('Content-Type', /json/) 11 | .expect(404, done); 12 | }); 13 | }); 14 | 15 | describe('GET /', () => { 16 | it('responds with a json message', (done) => { 17 | request(app) 18 | .get('/') 19 | .set('Accept', 'application/json') 20 | .expect('Content-Type', /json/) 21 | .expect(200, { 22 | message: '🦄🌈✨👋🌎🌍🌏✨🌈🦄' 23 | }, done); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/auth/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require("passport"); 2 | const passportJwt = require("passport-jwt"); 3 | const ExtractJwt = passportJwt.ExtractJwt; 4 | const StrategyJwt = passportJwt.Strategy; 5 | const User = require("../models/user"); 6 | 7 | passport.use( 8 | new StrategyJwt( 9 | { 10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 11 | secretOrKey: process.env.JWT_SECRET, 12 | }, 13 | function (jwtPayload, done) { 14 | return User.findOne({ where: { id: jwtPayload.id } }) 15 | .then((user) => { 16 | return done(null, user); 17 | }) 18 | .catch((err) => { 19 | return done(err); 20 | }); 21 | } 22 | ) 23 | ); 24 | -------------------------------------------------------------------------------- /test/api.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const app = require('../src/app'); 4 | 5 | describe('GET /api/v1', () => { 6 | it('responds with a json message', (done) => { 7 | request(app) 8 | .get('/api/v1') 9 | .set('Accept', 'application/json') 10 | .expect('Content-Type', /json/) 11 | .expect(200, { 12 | message: 'API - 👋🌎🌍🌏' 13 | }, done); 14 | }); 15 | }); 16 | 17 | describe('GET /api/v1/emojis', () => { 18 | it('responds with a json message', (done) => { 19 | request(app) 20 | .get('/api/v1/emojis') 21 | .set('Accept', 'application/json') 22 | .expect('Content-Type', /json/) 23 | .expect(200, ['😀', '😳', '🙄'], done); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const morgan = require("morgan"); 3 | const helmet = require("helmet"); 4 | const cors = require("cors"); 5 | const path = require("path"); 6 | const bodyParser = require("body-parser"); 7 | require("dotenv").config(); 8 | require("./auth/passport"); 9 | 10 | require("./models/user"); 11 | 12 | const middlewares = require("./middlewares"); 13 | const api = require("./api"); 14 | 15 | const app = express(); 16 | 17 | app.use(bodyParser.urlencoded({ extended: true })); 18 | app.use(bodyParser.json()); 19 | 20 | app.use(morgan("dev")); 21 | app.use(helmet()); 22 | app.use(cors()); 23 | app.use(express.json()); 24 | 25 | app.get("/", (req, res) => { 26 | res.sendFile(path.join(__dirname, "pages/homepage.html")); 27 | }); 28 | 29 | app.use("/api/v1", api); 30 | 31 | app.use(middlewares.notFound); 32 | app.use(middlewares.errorHandler); 33 | 34 | module.exports = app; 35 | -------------------------------------------------------------------------------- /benchmarks/basic-get.js: -------------------------------------------------------------------------------- 1 | const autocannon = require("autocannon"); 2 | require("dotenv").config(); 3 | 4 | function startBench() { 5 | const url = "http://localhost:" + process.env.PORT || 5000; 6 | 7 | const args = process.argv.slice(2); 8 | const numConnections = args[0] || 1000; 9 | const maxConnectionRequests = args[1] || 1000; 10 | 11 | const instance = autocannon( 12 | { 13 | url, 14 | connections: numConnections, 15 | duration: 10, 16 | maxConnectionRequests, 17 | headers: { 18 | "content-type": "application/json", 19 | }, 20 | requests: [ 21 | { 22 | method: "GET", 23 | path: "/", 24 | }, 25 | ], 26 | }, 27 | finishedBench 28 | ); 29 | 30 | autocannon.track(instance); 31 | 32 | function finishedBench(err, res) { 33 | console.log("Finished Bench", err, res); 34 | } 35 | } 36 | 37 | startBench(); 38 | -------------------------------------------------------------------------------- /src/api/login.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const User = require("../models/user"); 3 | const jwt = require("jsonwebtoken"); 4 | 5 | const router = express.Router(); 6 | 7 | router.post("/login", async (req, res) => { 8 | const { email, password } = req.body; 9 | 10 | const userWithEmail = await User.findOne({ where: { email } }).catch( 11 | (err) => { 12 | console.log("Error: ", err); 13 | } 14 | ); 15 | 16 | if (!userWithEmail) 17 | return res 18 | .status(400) 19 | .json({ message: "Email or password does not match!" }); 20 | 21 | if (userWithEmail.password !== password) 22 | return res 23 | .status(400) 24 | .json({ message: "Email or password does not match!" }); 25 | 26 | const jwtToken = jwt.sign( 27 | { id: userWithEmail.id, email: userWithEmail.email }, 28 | process.env.JWT_SECRET 29 | ); 30 | 31 | res.json({ message: "Welcome Back!", token: jwtToken }); 32 | }); 33 | 34 | module.exports = router; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2020 CJ R. 2 | 3 | Permission is hereby granted, free 4 | of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-api-starter", 3 | "version": "1.2.0", 4 | "description": " A basic starter for an express.js API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "dev": "nodemon src/index.js", 9 | "lint": "eslint --fix src", 10 | "test": "mocha --exit" 11 | }, 12 | "keywords": [], 13 | "author": "Islem Maboud", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/w3cj/express-api-starter.git" 17 | }, 18 | "license": "MIT", 19 | "dependencies": { 20 | "body-parser": "^1.19.0", 21 | "cors": "^2.8.5", 22 | "dotenv": "^8.2.0", 23 | "express": "^4.17.1", 24 | "helmet": "^4.2.0", 25 | "jsonwebtoken": "^8.5.1", 26 | "morgan": "^1.10.0", 27 | "mysql2": "^2.2.5", 28 | "passport": "^0.4.1", 29 | "passport-jwt": "^4.0.0", 30 | "sequelize": "^6.3.5" 31 | }, 32 | "devDependencies": { 33 | "autocannon": "^7.3.0", 34 | "eslint": "^7.14.0", 35 | "eslint-config-airbnb-base": "^14.2.1", 36 | "eslint-plugin-import": "^2.22.1", 37 | "mocha": "^8.2.1", 38 | "nodemon": "^2.0.6", 39 | "supertest": "^6.0.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /benchmarks/login-register.js: -------------------------------------------------------------------------------- 1 | const autocannon = require("autocannon"); 2 | require("dotenv").config(); 3 | 4 | const usersData = require("./users.json"); 5 | 6 | function startBench() { 7 | const url = "http://localhost:" + process.env.PORT || 5000; 8 | 9 | const args = process.argv.slice(2); 10 | const numConnections = args[0] || 1000; 11 | const maxConnectionRequests = args[1] || 1000; 12 | 13 | let requestNumber = 0; 14 | 15 | const instance = autocannon( 16 | { 17 | url, 18 | connections: numConnections, 19 | duration: 10, 20 | maxConnectionRequests, 21 | headers: { 22 | "content-type": "application/json", 23 | }, 24 | requests: [ 25 | { 26 | method: "POST", 27 | path: "/api/v1/register", 28 | setupRequest: function (request) { 29 | console.log("Request Number: ", requestNumber + 1); 30 | 31 | request.body = JSON.stringify(usersData[requestNumber]); 32 | 33 | requestNumber++; 34 | 35 | return request; 36 | }, 37 | }, 38 | ], 39 | }, 40 | finishedBench 41 | ); 42 | 43 | autocannon.track(instance); 44 | 45 | function finishedBench(err, res) { 46 | console.log("Finished Bench", err, res); 47 | } 48 | } 49 | 50 | startBench(); 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express API Starter 2 | 3 | Includes API Server utilities: 4 | 5 | * [morgan](https://www.npmjs.com/package/morgan) 6 | * HTTP request logger middleware for node.js 7 | * [helmet](https://www.npmjs.com/package/helmet) 8 | * Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it can help! 9 | * [dotenv](https://www.npmjs.com/package/dotenv) 10 | * Dotenv is a zero-dependency module that loads environment variables from a `.env` file into `process.env` 11 | 12 | Development utilities: 13 | 14 | * [nodemon](https://www.npmjs.com/package/nodemon) 15 | * nodemon is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected. 16 | * [eslint](https://www.npmjs.com/package/eslint) 17 | * ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. 18 | * [mocha](https://www.npmjs.com/package/mocha) 19 | * ☕️ Simple, flexible, fun JavaScript test framework for Node.js & The Browser ☕️ 20 | * [supertest](https://www.npmjs.com/package/supertest) 21 | * HTTP assertions made easy via superagent. 22 | 23 | ## Setup 24 | 25 | ``` 26 | npm install 27 | ``` 28 | 29 | ## Lint 30 | 31 | ``` 32 | npm run lint 33 | ``` 34 | 35 | ## Test 36 | 37 | ``` 38 | npm run test 39 | ``` 40 | 41 | ## Development 42 | 43 | ``` 44 | npm run dev 45 | ``` 46 | -------------------------------------------------------------------------------- /src/api/register.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const User = require("../models/user"); 3 | 4 | const router = express.Router(); 5 | 6 | router.post("/register", async (req, res) => { 7 | const { fullName, email, password } = req.body; 8 | 9 | if (!fullName || !email || !password) { 10 | return res 11 | .status(400) 12 | .json({ message: "FullName, email and password is required!" }); 13 | } 14 | 15 | const alreadyExistsUser = await User.findOne({ where: { email } }).catch( 16 | (err) => { 17 | console.log("Error: ", err); 18 | } 19 | ); 20 | 21 | if (alreadyExistsUser) { 22 | return res.status(409).json({ message: "User with email already exists!" }); 23 | } 24 | 25 | const newUser = new User({ fullName, email, password }); 26 | const savedUser = await newUser.save().catch((err) => { 27 | res.status(500).json({ error: "Cannot register user at the moment!" }); 28 | }); 29 | 30 | if (savedUser) res.json({ message: "Thanks for registering" }); 31 | }); 32 | 33 | module.exports = router; 34 | 35 | /** 36 | 37 | [ 38 | '{{repeat(1000)}}', 39 | { 40 | fullName: '{{firstName() + " " + surname()}}', 41 | email: '{{random("test123", "t123ffgd", "hola234", "john34fd2", "pass123456", "321pass345", "pa$$w0rd342", "name3435", "anonym443$f", "rocks434tar", "Svveet344", "123letitbe321", "candy34corn", "spaceX0X", "can434tessting", "1337xheaven", "animelover22", "anime0wow")}}', 42 | password: '{{guid()}}' 43 | } 44 | ] 45 | 46 | 47 | */ 48 | -------------------------------------------------------------------------------- /src/pages/homepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce elementum, 13 | ex eget vehicula semper, nisi urna porttitor erat, eu ultrices felis magna 14 | eu justo. Sed vitae dui lacinia, mollis erat id, sollicitudin sapien. 15 | Maecenas sed pulvinar nulla. Vivamus vel leo ligula. Suspendisse elementum 16 | rhoncus iaculis. Nunc convallis sagittis tortor, id hendrerit nisl 17 | eleifend et. In vitae sapien in leo vehicula suscipit. Vivamus eleifend, 18 | felis at dictum accumsan, ante est consequat diam, non molestie dolor orci 19 | eu mi. Nam sit amet nulla congue lectus porttitor rutrum. In ullamcorper 20 | malesuada ligula, sit amet elementum massa dapibus vel. Phasellus sit amet 21 | purus molestie, tincidunt lacus ac, rutrum purus. Morbi eget gravida 22 | dolor. Quisque eget placerat diam, sit amet dignissim arcu. Mauris 23 | volutpat iaculis orci vitae scelerisque. Nullam eu vehicula eros. In 24 | congue, augue id commodo sollicitudin, elit eros faucibus est, et 25 | tincidunt eros arcu a leo. Ut gravida arcu id lacus porttitor, sed sodales 26 | velit interdum. Donec placerat odio id urna faucibus euismod. Vestibulum 27 | volutpat lacus in aliquam commodo. Pellentesque eget ultrices urna. 28 | Suspendisse quis hendrerit odio, eu dapibus ante. Donec sed magna at leo 29 | gravida dapibus. Phasellus fringilla nibh ut ipsum pellentesque laoreet. 30 | Quisque et lectus vel nisi congue convallis. Cras luctus pellentesque orci 31 | sed vestibulum. Aliquam leo enim, sollicitudin vel est eleifend, pulvinar 32 | faucibus purus. Nulla purus eros, suscipit a sem a, aliquam ullamcorper 33 | orci. Donec vel laoreet dolor. Aliquam id eros sit amet enim finibus 34 | consectetur. Integer sodales neque id varius commodo. Quisque finibus 35 | sapien ut laoreet gravida. In ac lorem ut sem facilisis tempus et vitae 36 | dui. Orci varius natoque penatibus et magnis dis parturient montes, 37 | nascetur ridiculus mus. Mauris mauris ex, maximus et vehicula nec, cursus 38 | at massa. Maecenas quis diam dolor. Duis bibendum lacinia libero, quis 39 | ornare urna cursus a. Curabitur lobortis orci mi, interdum rhoncus nibh 40 | ultrices ut. Nam at luctus lorem, at elementum orci. Lorem ipsum dolor sit 41 | amet, consectetur adipiscing elit. Nulla sagittis pellentesque sapien non 42 | aliquam. 43 |
44 | 45 | 46 | --------------------------------------------------------------------------------