├── .gitignore ├── LICENSE ├── README.md ├── REST ├── api.http ├── auth.http ├── profile.http └── user.http ├── banner.png ├── banner.psd ├── config ├── database.ts └── default.json ├── package.json ├── src ├── middleware │ └── auth.ts ├── models │ ├── Profiles.ts │ └── User.ts ├── routes │ └── api │ │ ├── auth.ts │ │ ├── profile.ts │ │ └── user.ts ├── server.ts └── types │ ├── Payload.ts │ └── Request.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paul Cham 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 | # Mongoose Node.js Express TypeScript application boilerplate with best practices for API development. 2 | 3 | ![image](https://user-images.githubusercontent.com/10678997/57565876-01281b00-73f8-11e9-8d86-911faa4a6c0f.png) 4 | 5 | The main purpose of this repository is to show a good end-to-end project setup and workflow for writing a strongly-typed [Mongoose](https://mongoosejs.com/) [Node.js](https://nodejs.org/en/) [Express](https://expressjs.com/) code in [TypeScript](https://www.typescriptlang.org/) complete with middleware, models, routes, and types. 6 | 7 | This example comes with a complete REST API to handle Authentication and CRUD features on Users and their corresponding Profile. You may view the API documentation on the [Wiki](https://github.com/polcham/mongoose-express-ts/wiki). 8 | 9 | # Why TypeScript? 10 | 11 | While it's true that developing applications on an Untyped language such as **JavaScript**, is easier to learn and is faster to develop, it will undeniably get harder and harder to grasp as the application grows in scale. This in turn, leads to more run-time errors consuming more development hours, as the team gets accustomed to the growing codebase. And this is what this boilerplate hopes to achieve. By using the **TypeScript** standard, you'll have better team and code stability with **Interface Oriented Development**, leading to better standardized codes. TypeScript allows developers to focus more on exposed Interfaces or API, rather than having to know all the code by heart. This makes the codebase easier to maintain with big teams, especially if those teams are composed of developers of different skill levels. 12 | 13 | # Why Mongoose? 14 | 15 | [Mongoose](https://mongoosejs.com/) is an object document modeling (ODM) layer that sits on top of Node's MongoDB driver. If you are coming from SQL, it is similar to object relational mapping (ORM) for a relational database. While it is not required to use Mongoose with MongoDB, it is generally a good idea for various reasons. Since MongoDB is a denormalized NoSQL database, its inherently schema-less design means documents will have varying sets of fields with different data types. This provides your data model with as much flexibility as you wanted over time, however, it can be difficult to cope with coming from a SQL background. Mongoose defines a schema for your data models so your documents follow a specific structure with pre-defined data types. On top of that, Mongoose provides built-in type casting, validation, query building, and business logic hooks out-of-the-box which saves developers the pain of writing boilerplates for MongoDB. 16 | 17 | # Prerequisites 18 | 19 | To build and run this app locally you will need a few things: 20 | 21 | - Install [Node.js](https://nodejs.org/en/) 22 | - Install [VS Code](https://code.visualstudio.com/) 23 | - You will need a **MongoDB server** which could either be hosted locally or online. 24 | - Once you know your MongoDB URI, set the value of **mongoURI** in **config/default.json**. 25 | 26 | # Getting started 27 | 28 | - Clone the repository 29 | 30 | ``` 31 | git clone --depth=1 https://github.com/polcham/mongoose-express-ts.git 32 | ``` 33 | 34 | - Install dependencies 35 | 36 | ``` 37 | cd 38 | npm install 39 | npm run tsc 40 | ``` 41 | 42 | - Build and run the project with auto reload (nodemon) 43 | 44 | ``` 45 | npm run server 46 | ``` 47 | 48 | - Build and run the project 49 | 50 | ``` 51 | npm run start 52 | ``` 53 | 54 | Finally, navigate to `http://localhost:5000/` and you should see the API running! 55 | 56 | ## Project Structure 57 | 58 | The most obvious difference in a TypeScript + Node project is the folder structure. In a TypeScript project, it's best to have separate _source_ and _distributable_ files. TypeScript (`.ts`) files live in your `src` folder and after compilation are output as JavaScript (`.js`) in the `dist` folder. 59 | 60 | The full folder structure of this app is explained below: 61 | 62 | > **Note!** Make sure you have already built the app using `npm run start` 63 | 64 | | Name | Description | 65 | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | 66 | | **config** | Contains config environment to be used by the config package, such as MongoDB URI, jwtSecret, and etc. | 67 | | **dist** | Contains the distributable (or output) from your TypeScript build | 68 | | **node_modules** | Contains all your npm dependencies | 69 | | **REST** | Contains all API requests to test the routes, used with [REST Client VSCode extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) | 70 | | **src** | Contains your source code that will be compiled to the dist dir | 71 | | **src/middleware** | Contains the middlewares to intercept requests | 72 | | **src/models** | Models define Mongoose schemas that will be used in storing and retrieving data from MongoDB | 73 | | **src/routes** | Routes define the endpoints of your API | 74 | | **src/types** | Contains all your custom types to better handle type checking with TypeScript | 75 | | **src/server.ts** | Entry point to your express app | 76 | | package.json | File that contains npm dependencies as well as [build scripts](#what-if-a-library-isnt-on-definitelytyped) | 77 | | tsconfig.json | Config settings for compiling server code written in TypeScript | 78 | | tslint.json | Config settings for TSLint code style checking | 79 | 80 | ### Configuring TypeScript compilation 81 | 82 | TypeScript uses the file `tsconfig.json` to adjust project compile options. 83 | Let's dissect this project's `tsconfig.json`, starting with the `compilerOptions` which details how your project is compiled. 84 | 85 | ```json 86 | "compilerOptions": { 87 | "module": "commonjs", 88 | "esModuleInterop": true, 89 | "target": "es6", 90 | "noImplicitAny": true, 91 | "moduleResolution": "node", 92 | "sourceMap": true, 93 | "outDir": "dist", 94 | "baseUrl": ".", 95 | "paths": { 96 | "*": ["node_modules/*", "src/types/*"] 97 | } 98 | } 99 | ``` 100 | 101 | | `compilerOptions` | Description | 102 | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | 103 | | `"module": "commonjs"` | The **output** module type (in your `.js` files). Node uses commonjs, so that is what we use | 104 | | `"esModuleInterop": true,` | Allows usage of an alternate module import syntax: `import foo from 'foo';` | 105 | | `"target": "es6"` | The output language level. Node supports ES6, so we can target that here | 106 | | `"noImplicitAny": true` | Enables a stricter setting which throws errors when something has a default `any` value | 107 | | `"moduleResolution": "node"` | TypeScript attempts to mimic Node's module resolution strategy. Read more [here](https://www.typescriptlang.org/docs/handbook/module-resolution.html#node) | 108 | | `"sourceMap": true` | We want source maps to be output along side our JavaScript. See the [debugging](#debugging) section | 109 | | `"outDir": "dist"` | Location to output `.js` files after compilation | 110 | | `"baseUrl": "."` | Part of configuring module resolution. See [path mapping section](#installing-dts-files-from-definitelytyped) | 111 | | `paths: {...}` | Part of configuring module resolution. See [path mapping section](#installing-dts-files-from-definitelytyped) | 112 | 113 | The rest of the file define the TypeScript project context. 114 | The project context is basically a set of options that determine which files are compiled when the compiler is invoked with a specific `tsconfig.json`. 115 | In this case, we use the following to define our project context: 116 | 117 | ```json 118 | "include": [ 119 | "src/**/*" 120 | ] 121 | ``` 122 | 123 | `include` takes an array of glob patterns of files to include in the compilation. This project is fairly simple and all of our .ts files are under the `src` folder. 124 | 125 | ### Running the build 126 | 127 | All the different build steps are orchestrated via [npm scripts](https://docs.npmjs.com/misc/scripts). 128 | Npm scripts basically allow us to call (and chain) terminal commands via npm. 129 | This is nice because most JavaScript tools have easy to use command line utilities allowing us to not need grunt or gulp to manage our builds. 130 | If you open `package.json`, you will see a `scripts` section with all the different scripts you can call. 131 | To call a script, simply run `npm run ` from the command line. 132 | You'll notice that npm scripts can call each other which makes it easy to compose complex builds out of simple individual build scripts. 133 | Below is a list of all the scripts this template has available: 134 | 135 | | Npm Script | Description | 136 | | -------------- | --------------------------------------------------------------------------------------------- | 137 | | `tsc` | Transpiles TypeScript codes to JavaScript. | 138 | | `watch-tsc` | Transpiles TypeScript codes to JavaScript, with auto reload. | 139 | | `deploy` | Runs node on `dist/server.js` which is the app's entry point. | 140 | | `watch-deploy` | Runs node on `dist/server.js` which is the app's entry point, with auto reload. | 141 | | `server` | Transpiles TypeScript codes to JavaScript then run node on `dist/server.js` with auto reload. | 142 | | `start` | Transpiles TypeScript codes to JavaScript then run node on `dist/server.js`. | 143 | 144 | Since we're developing with TypeScript, it is important for the codes to be transpiled first to JavaScript before running the node server. It is best to deploy the app using: `npm run server` or `npm run start` command. 145 | 146 | ### VSCode Extensions 147 | 148 | To enhance your development experience while working in VSCode, I provided you with a list of suggested extensions while working on this project: 149 | 150 | - [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) 151 | - [Version Lens](https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens) 152 | - [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 153 | - [indent-rainbow](https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow) 154 | - [Material Icon Theme](https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme) 155 | 156 | # Dependencies 157 | 158 | Dependencies are managed through `package.json`. 159 | In that file you'll find two sections: 160 | 161 | ## `dependencies` 162 | 163 | | Package | Description | 164 | | ----------------- | ----------------------------------------------- | 165 | | bcryptjs | Library for hashing and salting user passwords. | 166 | | config | Universal configurations for your app. | 167 | | express | Node.js web framework. | 168 | | express-validator | Easy form validation for Express. | 169 | | gravatar | Generate Gravatar URLs based on gravatar specs. | 170 | | http-status-codes | HTTP status codes constants. | 171 | | jsonwebtoken | JsonWebToken implementation for Node.js. | 172 | | mongoose | MongoDB modeling tool in an async environment. | 173 | | request | Simplified HTTP client for Node.js. | 174 | | typescript | Typed superset of JavaScript. | 175 | 176 | ## `devDependencies` 177 | 178 | Since TypeScript is used, dependencies should be accompanied with their corresponding DefinitelyTyped @types package. 179 | 180 | | Package | Description | 181 | | ------------------- | --------------------------------------- | 182 | | @types/bcryptjs | DefinitelyTyped for bcryptjs | 183 | | @types/config | DefinitelyTyped for config | 184 | | @types/express | DefinitelyTyped for express | 185 | | @types/gravatar | DefinitelyTyped for gravatar | 186 | | @types/jsonwebtoken | DefinitelyTyped for jsonwebtoken | 187 | | @types/mongoose | DefinitelyTyped for mongoose | 188 | | concurrently | Run multiple commands concurrently | 189 | | nodemon | Reload node application on code changes | 190 | 191 | To install or update these dependencies you can use `npm install` or `npm update`. 192 | -------------------------------------------------------------------------------- /REST/api.http: -------------------------------------------------------------------------------- 1 | ### Variables 2 | 3 | ### Test Base API 4 | GET http://localhost:5000 -------------------------------------------------------------------------------- /REST/auth.http: -------------------------------------------------------------------------------- 1 | ### Variables 2 | @token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1Y2Q2ODMxZjFhY2I0ZjBiNjkxYzRlZjYiLCJpYXQiOjE1NTc1NjIxNDMsImV4cCI6MTU1NzkyMjE0M30.BP7n27AVY9MKTz1ViHMJWOVqQGMktJmT8AJWrZuQoP0 3 | @login = { "email": "hello@email.com", "password": "password" } 4 | 5 | ### Get authenticated user given the token 6 | GET http://localhost:5000/api/auth 7 | x-auth-token: {{token}} 8 | 9 | ### Login user and get token 10 | POST http://localhost:5000/api/auth 11 | content-type: application/json 12 | 13 | {{login}} -------------------------------------------------------------------------------- /REST/profile.http: -------------------------------------------------------------------------------- 1 | ### Variables 2 | @token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1Y2Q2ODMxZjFhY2I0ZjBiNjkxYzRlZjYiLCJpYXQiOjE1NTc1NjIxNDMsImV4cCI6MTU1NzkyMjE0M30.BP7n27AVY9MKTz1ViHMJWOVqQGMktJmT8AJWrZuQoP0 3 | @profile = { "firstName": "John", "lastName": "Doe", "username": "john.doe" } 4 | @userId = 5cd6831f1acb4f0b691c4ef6 5 | 6 | ### Get current user's profile 7 | GET http://localhost:5000/api/profile/me 8 | x-auth-token: {{token}} 9 | 10 | ### Create or update user's profile 11 | POST http://localhost:5000/api/profile 12 | x-auth-token: {{token}} 13 | content-type: application/json 14 | 15 | {{profile}} 16 | 17 | ### Get all profiles 18 | GET http://localhost:5000/api/profile 19 | 20 | ### Get profile by userId 21 | GET http://localhost:5000/api/profile/user/{{userId}} 22 | 23 | ### Delete profile and user 24 | DELETE http://localhost:5000/api/profile 25 | x-auth-token: {{token}} -------------------------------------------------------------------------------- /REST/user.http: -------------------------------------------------------------------------------- 1 | ### Variables 2 | @user = { "email": "hello@email.com", "password": "password"} 3 | 4 | ### Register user given their email and password, returns the token upon successful registration 5 | POST http://localhost:5000/api/user 6 | content-type: application/json 7 | 8 | {{user}} -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunnysidelabs/mongoose-express-ts/4e806c4360acebd12dff0f71c20f4eec69d09162/banner.png -------------------------------------------------------------------------------- /banner.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunnysidelabs/mongoose-express-ts/4e806c4360acebd12dff0f71c20f4eec69d09162/banner.psd -------------------------------------------------------------------------------- /config/database.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import { connect } from "mongoose"; 3 | 4 | const connectDB = async () => { 5 | try { 6 | const mongoURI: string = config.get("mongoURI"); 7 | await connect(mongoURI); 8 | console.log("MongoDB Connected..."); 9 | } catch (err) { 10 | console.error(err.message); 11 | // Exit process with failure 12 | process.exit(1); 13 | } 14 | }; 15 | 16 | export default connectDB; 17 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongoURI": "mongodb+srv://root:1234@devconnector-ejkgk.mongodb.net/test?retryWrites=true", 3 | "jwtSecret": "jwtSecretToken", 4 | "jwtExpiration": 360000 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-express-ts", 3 | "version": "1.0.0", 4 | "description": "Mongoose Node.js Express TypeScript application boilerplate with best practices for API development.", 5 | "author": "Paul Cham", 6 | "license": "MIT", 7 | "scripts": { 8 | "tsc": "tsc", 9 | "watch-tsc": "tsc -w", 10 | "deploy": "node dist/src/server.js", 11 | "watch-deploy": "nodemon dist/src/server.js", 12 | "server": "concurrently \"npm run watch-tsc\" \"npm run watch-deploy\"", 13 | "start": "npm run deploy", 14 | "heroku-postbuild": "npm run tsc" 15 | }, 16 | "dependencies": { 17 | "bcryptjs": "^2.4.3", 18 | "config": "^3.3.8", 19 | "express": "^4.18.2", 20 | "express-validator": "^6.14.2", 21 | "gravatar": "^1.8.2", 22 | "http-status-codes": "^2.2.0", 23 | "jsonwebtoken": "^8.5.1", 24 | "mongoose": "^6.6.5", 25 | "request": "^2.88.2", 26 | "typescript": "^4.8.4" 27 | }, 28 | "devDependencies": { 29 | "@types/bcryptjs": "^2.4.2", 30 | "@types/config": "3.3.0", 31 | "@types/express": "^4.17.14", 32 | "@types/gravatar": "^1.8.3", 33 | "@types/jsonwebtoken": "^8.5.9", 34 | "@types/mongoose": "^5.11.97", 35 | "concurrently": "^7.4.0", 36 | "nodemon": "^2.0.20" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import { Response, NextFunction } from "express"; 3 | import HttpStatusCodes from "http-status-codes"; 4 | import jwt from "jsonwebtoken"; 5 | 6 | import Payload from "../types/Payload"; 7 | import Request from "../types/Request"; 8 | 9 | export default function(req: Request, res: Response, next: NextFunction) { 10 | // Get token from header 11 | const token = req.header("x-auth-token"); 12 | 13 | // Check if no token 14 | if (!token) { 15 | return res 16 | .status(HttpStatusCodes.UNAUTHORIZED) 17 | .json({ msg: "No token, authorization denied" }); 18 | } 19 | // Verify token 20 | try { 21 | const payload: Payload | any = jwt.verify(token, config.get("jwtSecret")); 22 | req.userId = payload.userId; 23 | next(); 24 | } catch (err) { 25 | res 26 | .status(HttpStatusCodes.UNAUTHORIZED) 27 | .json({ msg: "Token is not valid" }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/models/Profiles.ts: -------------------------------------------------------------------------------- 1 | import { Document, model, Schema } from "mongoose"; 2 | import { IUser } from "./User"; 3 | 4 | /** 5 | * Type to model the Profile Schema for TypeScript. 6 | * @param user:ref => User._id 7 | * @param firstName:string 8 | * @param lastName:string 9 | * @param username:string 10 | */ 11 | 12 | export type TProfile = { 13 | user: IUser["_id"]; 14 | firstName: string; 15 | lastName: string; 16 | username: string; 17 | }; 18 | 19 | /** 20 | * Mongoose Document based on TProfile for TypeScript. 21 | * https://mongoosejs.com/docs/documents.html 22 | * 23 | * TProfile 24 | * @param user:ref => User._id 25 | * @param firstName:string 26 | * @param lastName:string 27 | * @param username:string 28 | */ 29 | 30 | export interface IProfile extends TProfile, Document {} 31 | 32 | const profileSchema: Schema = new Schema({ 33 | user: { 34 | type: Schema.Types.ObjectId, 35 | ref: "User", 36 | }, 37 | firstName: { 38 | type: String, 39 | required: true, 40 | }, 41 | lastName: { 42 | type: String, 43 | required: true, 44 | }, 45 | username: { 46 | type: String, 47 | required: true, 48 | unique: true, 49 | }, 50 | date: { 51 | type: Date, 52 | default: Date.now, 53 | }, 54 | }); 55 | 56 | /** 57 | * Mongoose Model based on TProfile for TypeScript. 58 | * https://mongoosejs.com/docs/models.html 59 | * 60 | * TProfile 61 | * @param user:ref => User._id 62 | * @param firstName:string 63 | * @param lastName:string 64 | * @param username:string 65 | */ 66 | 67 | const Profile = model("Profile", profileSchema); 68 | 69 | export default Profile; 70 | -------------------------------------------------------------------------------- /src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { Document, model, Schema } from "mongoose"; 2 | 3 | /** 4 | * Type to model the User Schema for TypeScript. 5 | * @param email:string 6 | * @param password:string 7 | * @param avatar:string 8 | */ 9 | 10 | export type TUser = { 11 | email: string; 12 | password: string; 13 | avatar: string; 14 | }; 15 | 16 | /** 17 | * Mongoose Document based on TUser for TypeScript. 18 | * https://mongoosejs.com/docs/documents.html 19 | * 20 | * TUser 21 | * @param email:string 22 | * @param password:string 23 | * @param avatar:string 24 | */ 25 | 26 | export interface IUser extends TUser, Document {} 27 | 28 | const userSchema: Schema = new Schema({ 29 | email: { 30 | type: String, 31 | required: true, 32 | unique: true, 33 | }, 34 | password: { 35 | type: String, 36 | required: true, 37 | }, 38 | avatar: { 39 | type: String, 40 | }, 41 | date: { 42 | type: Date, 43 | default: Date.now, 44 | }, 45 | }); 46 | 47 | /** 48 | * Mongoose Model based on TUser for TypeScript. 49 | * https://mongoosejs.com/docs/models.html 50 | * 51 | * TUser 52 | * @param email:string 53 | * @param password:string 54 | * @param avatar:string 55 | */ 56 | 57 | const User = model("User", userSchema); 58 | 59 | export default User; 60 | -------------------------------------------------------------------------------- /src/routes/api/auth.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcryptjs"; 2 | import config from "config"; 3 | import { Router, Response } from "express"; 4 | import { check, validationResult } from "express-validator"; 5 | import HttpStatusCodes from "http-status-codes"; 6 | import jwt from "jsonwebtoken"; 7 | 8 | import auth from "../../middleware/auth"; 9 | import Payload from "../../types/Payload"; 10 | import Request from "../../types/Request"; 11 | import User, { IUser } from "../../models/User"; 12 | 13 | const router: Router = Router(); 14 | 15 | // @route GET api/auth 16 | // @desc Get authenticated user given the token 17 | // @access Private 18 | router.get("/", auth, async (req: Request, res: Response) => { 19 | try { 20 | const user: IUser = await User.findById(req.userId).select("-password"); 21 | res.json(user); 22 | } catch (err) { 23 | console.error(err.message); 24 | res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).send("Server Error"); 25 | } 26 | }); 27 | 28 | // @route POST api/auth 29 | // @desc Login user and get token 30 | // @access Public 31 | router.post( 32 | "/", 33 | [ 34 | check("email", "Please include a valid email").isEmail(), 35 | check("password", "Password is required").exists(), 36 | ], 37 | async (req: Request, res: Response) => { 38 | const errors = validationResult(req); 39 | if (!errors.isEmpty()) { 40 | return res 41 | .status(HttpStatusCodes.BAD_REQUEST) 42 | .json({ errors: errors.array() }); 43 | } 44 | 45 | const { email, password } = req.body; 46 | try { 47 | let user: IUser = await User.findOne({ email }); 48 | 49 | if (!user) { 50 | return res.status(HttpStatusCodes.BAD_REQUEST).json({ 51 | errors: [ 52 | { 53 | msg: "Invalid Credentials", 54 | }, 55 | ], 56 | }); 57 | } 58 | 59 | const isMatch = await bcrypt.compare(password, user.password); 60 | 61 | if (!isMatch) { 62 | return res.status(HttpStatusCodes.BAD_REQUEST).json({ 63 | errors: [ 64 | { 65 | msg: "Invalid Credentials", 66 | }, 67 | ], 68 | }); 69 | } 70 | 71 | const payload: Payload = { 72 | userId: user.id, 73 | }; 74 | 75 | jwt.sign( 76 | payload, 77 | config.get("jwtSecret"), 78 | { expiresIn: config.get("jwtExpiration") }, 79 | (err, token) => { 80 | if (err) throw err; 81 | res.json({ token }); 82 | } 83 | ); 84 | } catch (err) { 85 | console.error(err.message); 86 | res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).send("Server Error"); 87 | } 88 | } 89 | ); 90 | 91 | export default router; 92 | -------------------------------------------------------------------------------- /src/routes/api/profile.ts: -------------------------------------------------------------------------------- 1 | import { Router, Response } from "express"; 2 | import { check, validationResult } from "express-validator"; 3 | import HttpStatusCodes from "http-status-codes"; 4 | 5 | import auth from "../../middleware/auth"; 6 | import Profile, { TProfile, IProfile } from "../../models/Profiles"; 7 | import Request from "../../types/Request"; 8 | import User, { IUser } from "../../models/User"; 9 | 10 | const router: Router = Router(); 11 | 12 | // @route GET api/profile/me 13 | // @desc Get current user's profile 14 | // @access Private 15 | router.get("/me", auth, async (req: Request, res: Response) => { 16 | try { 17 | const profile: IProfile = await Profile.findOne({ 18 | user: req.userId, 19 | }).populate("user", ["avatar", "email"]); 20 | if (!profile) { 21 | return res.status(HttpStatusCodes.BAD_REQUEST).json({ 22 | errors: [ 23 | { 24 | msg: "There is no profile for this user", 25 | }, 26 | ], 27 | }); 28 | } 29 | 30 | res.json(profile); 31 | } catch (err) { 32 | console.error(err.message); 33 | res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).send("Server Error"); 34 | } 35 | }); 36 | 37 | // @route POST api/profile 38 | // @desc Create or update user's profile 39 | // @access Private 40 | router.post( 41 | "/", 42 | [ 43 | auth, 44 | check("firstName", "First Name is required").not().isEmpty(), 45 | check("lastName", "Last Name is required").not().isEmpty(), 46 | check("username", "Username is required").not().isEmpty(), 47 | ], 48 | async (req: Request, res: Response) => { 49 | const errors = validationResult(req); 50 | if (!errors.isEmpty()) { 51 | return res 52 | .status(HttpStatusCodes.BAD_REQUEST) 53 | .json({ errors: errors.array() }); 54 | } 55 | 56 | const { firstName, lastName, username } = req.body; 57 | 58 | // Build profile object based on TProfile 59 | const profileFields: TProfile = { 60 | user: req.userId, 61 | firstName, 62 | lastName, 63 | username, 64 | }; 65 | 66 | try { 67 | let user: IUser = await User.findOne({ _id: req.userId }); 68 | 69 | if (!user) { 70 | return res.status(HttpStatusCodes.BAD_REQUEST).json({ 71 | errors: [ 72 | { 73 | msg: "User not registered", 74 | }, 75 | ], 76 | }); 77 | } 78 | 79 | let profile: IProfile = await Profile.findOne({ user: req.userId }); 80 | if (profile) { 81 | // Update 82 | profile = await Profile.findOneAndUpdate( 83 | { user: req.userId }, 84 | { $set: profileFields }, 85 | { new: true } 86 | ); 87 | 88 | return res.json(profile); 89 | } 90 | 91 | // Create 92 | profile = new Profile(profileFields); 93 | 94 | await profile.save(); 95 | 96 | res.json(profile); 97 | } catch (err) { 98 | console.error(err.message); 99 | res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).send("Server Error"); 100 | } 101 | } 102 | ); 103 | 104 | // @route GET api/profile 105 | // @desc Get all profiles 106 | // @access Public 107 | router.get("/", async (_req: Request, res: Response) => { 108 | try { 109 | const profiles: IProfile[] = await Profile.find().populate("user", [ 110 | "avatar", 111 | "email", 112 | ]); 113 | res.json(profiles); 114 | } catch (err) { 115 | console.error(err.message); 116 | res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).send("Server Error"); 117 | } 118 | }); 119 | 120 | // @route GET api/profile/user/:userId 121 | // @desc Get profile by userId 122 | // @access Public 123 | router.get("/user/:userId", async (req: Request, res: Response) => { 124 | try { 125 | const profile: IProfile = await Profile.findOne({ 126 | user: req.params.userId, 127 | }).populate("user", ["avatar", "email"]); 128 | 129 | if (!profile) 130 | return res 131 | .status(HttpStatusCodes.BAD_REQUEST) 132 | .json({ msg: "Profile not found" }); 133 | 134 | res.json(profile); 135 | } catch (err) { 136 | console.error(err.message); 137 | if (err.kind === "ObjectId") { 138 | return res 139 | .status(HttpStatusCodes.BAD_REQUEST) 140 | .json({ msg: "Profile not found" }); 141 | } 142 | res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).send("Server Error"); 143 | } 144 | }); 145 | 146 | // @route DELETE api/profile 147 | // @desc Delete profile and user 148 | // @access Private 149 | router.delete("/", auth, async (req: Request, res: Response) => { 150 | try { 151 | // Remove profile 152 | await Profile.findOneAndRemove({ user: req.userId }); 153 | // Remove user 154 | await User.findOneAndRemove({ _id: req.userId }); 155 | 156 | res.json({ msg: "User removed" }); 157 | } catch (err) { 158 | console.error(err.message); 159 | res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).send("Server Error"); 160 | } 161 | }); 162 | 163 | export default router; 164 | -------------------------------------------------------------------------------- /src/routes/api/user.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcryptjs"; 2 | import config from "config"; 3 | import { Router, Response } from "express"; 4 | import { check, validationResult } from "express-validator"; 5 | import gravatar from "gravatar"; 6 | import HttpStatusCodes from "http-status-codes"; 7 | import jwt from "jsonwebtoken"; 8 | 9 | import Payload from "../../types/Payload"; 10 | import Request from "../../types/Request"; 11 | import User, { IUser, TUser } from "../../models/User"; 12 | 13 | const router: Router = Router(); 14 | 15 | // @route POST api/user 16 | // @desc Register user given their email and password, returns the token upon successful registration 17 | // @access Public 18 | router.post( 19 | "/", 20 | [ 21 | check("email", "Please include a valid email").isEmail(), 22 | check( 23 | "password", 24 | "Please enter a password with 6 or more characters" 25 | ).isLength({ min: 6 }), 26 | ], 27 | async (req: Request, res: Response) => { 28 | const errors = validationResult(req); 29 | if (!errors.isEmpty()) { 30 | return res 31 | .status(HttpStatusCodes.BAD_REQUEST) 32 | .json({ errors: errors.array() }); 33 | } 34 | 35 | const { email, password } = req.body; 36 | try { 37 | let user: IUser = await User.findOne({ email }); 38 | 39 | if (user) { 40 | return res.status(HttpStatusCodes.BAD_REQUEST).json({ 41 | errors: [ 42 | { 43 | msg: "User already exists", 44 | }, 45 | ], 46 | }); 47 | } 48 | 49 | const options: gravatar.Options = { 50 | s: "200", 51 | r: "pg", 52 | d: "mm", 53 | }; 54 | 55 | const avatar = gravatar.url(email, options); 56 | 57 | const salt = await bcrypt.genSalt(10); 58 | const hashed = await bcrypt.hash(password, salt); 59 | 60 | // Build user object based on TUser 61 | const userFields: TUser = { 62 | email, 63 | password: hashed, 64 | avatar, 65 | }; 66 | 67 | user = new User(userFields); 68 | 69 | await user.save(); 70 | 71 | const payload: Payload = { 72 | userId: user.id, 73 | }; 74 | 75 | jwt.sign( 76 | payload, 77 | config.get("jwtSecret"), 78 | { expiresIn: config.get("jwtExpiration") }, 79 | (err, token) => { 80 | if (err) throw err; 81 | res.json({ token }); 82 | } 83 | ); 84 | } catch (err) { 85 | console.error(err.message); 86 | res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).send("Server Error"); 87 | } 88 | } 89 | ); 90 | 91 | export default router; 92 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from "body-parser"; 2 | import express from "express"; 3 | 4 | import connectDB from "../config/database"; 5 | import auth from "./routes/api/auth"; 6 | import user from "./routes/api/user"; 7 | import profile from "./routes/api/profile"; 8 | 9 | const app = express(); 10 | 11 | // Connect to MongoDB 12 | connectDB(); 13 | 14 | // Express configuration 15 | app.set("port", process.env.PORT || 5000); 16 | app.use(bodyParser.json()); 17 | app.use(bodyParser.urlencoded({ extended: false })); 18 | 19 | // @route GET / 20 | // @desc Test Base API 21 | // @access Public 22 | app.get("/", (_req, res) => { 23 | res.send("API Running"); 24 | }); 25 | 26 | app.use("/api/auth", auth); 27 | app.use("/api/user", user); 28 | app.use("/api/profile", profile); 29 | 30 | const port = app.get("port"); 31 | const server = app.listen(port, () => 32 | console.log(`Server started on port ${port}`) 33 | ); 34 | 35 | export default server; 36 | -------------------------------------------------------------------------------- /src/types/Payload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Payload Object to be signed and verified by JWT. Used by the auth middleware to pass data to the request by token signing (jwt.sign) and token verification (jwt.verify). 3 | * @param userId:string 4 | */ 5 | type payload = { userId: string }; 6 | 7 | export default payload; 8 | -------------------------------------------------------------------------------- /src/types/Request.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express"; 2 | import Payload from "./Payload"; 3 | 4 | /** 5 | * Extended Express Request interface to pass Payload Object to the request. Used by the auth middleware to pass data to the request by token signing (jwt.sign) and token verification (jwt.verify). 6 | * @param userId:string 7 | */ 8 | type request = Request & Payload; 9 | 10 | export default request; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": ["node_modules/*", "src/types/*"] 13 | } 14 | }, 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, "check-space"], 5 | "indent": [true, "spaces"], 6 | "one-line": [true, "check-open-brace", "check-whitespace"], 7 | "no-var-keyword": true, 8 | "quotemark": [true, "double", "avoid-escape"], 9 | "semicolon": [true, "always", "ignore-bound-class-methods"], 10 | "whitespace": [ 11 | true, 12 | "check-branch", 13 | "check-decl", 14 | "check-operator", 15 | "check-module", 16 | "check-separator", 17 | "check-type" 18 | ], 19 | "typedef-whitespace": [ 20 | true, 21 | { 22 | "call-signature": "nospace", 23 | "index-signature": "nospace", 24 | "parameter": "nospace", 25 | "property-declaration": "nospace", 26 | "variable-declaration": "nospace" 27 | }, 28 | { 29 | "call-signature": "onespace", 30 | "index-signature": "onespace", 31 | "parameter": "onespace", 32 | "property-declaration": "onespace", 33 | "variable-declaration": "onespace" 34 | } 35 | ], 36 | "no-internal-module": true, 37 | "no-trailing-whitespace": true, 38 | "no-null-keyword": true, 39 | "prefer-const": true, 40 | "jsdoc-format": true 41 | } 42 | } 43 | --------------------------------------------------------------------------------