├── lib └── env │ ├── index.ts │ └── utils.ts ├── app ├── models │ ├── index.ts │ └── Sample.model.ts ├── middlewares │ ├── index.ts │ └── Validator.ts ├── repository │ ├── index.ts │ └── Sample.repository.ts ├── routes │ ├── index.ts │ ├── Jwt.route.ts │ ├── Router.ts │ └── Sample.route.ts ├── schemas │ ├── index.ts │ └── Sample.schemas.ts ├── services │ ├── index.ts │ ├── Jwt.service.ts │ └── Sample.service.ts └── controllers │ ├── index.ts │ ├── Controller.ts │ ├── Jwt.controller.ts │ └── Sample.controller.ts ├── test ├── mocha.opts └── Sample.test.ts ├── nodemon.json ├── tslint.json ├── Dockerfile ├── tsconfig.json ├── config ├── Database.ts ├── Router.ts └── Server.ts ├── .gitignore ├── .env ├── LICENSE ├── env.ts ├── Index.ts ├── package.json └── README.md /lib/env/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./utils"; 2 | -------------------------------------------------------------------------------- /app/models/index.ts: -------------------------------------------------------------------------------- 1 | import { Sample } from "./Sample.model"; 2 | 3 | export { Sample }; 4 | -------------------------------------------------------------------------------- /app/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | import { Validator } from "./Validator"; 2 | 3 | export { Validator }; 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require dotenv/config 3 | --timeout 20000 4 | --exit 5 | -------------------------------------------------------------------------------- /app/repository/index.ts: -------------------------------------------------------------------------------- 1 | import { SampleRepository } from "./Sample.repository"; 2 | 3 | export { SampleRepository }; 4 | -------------------------------------------------------------------------------- /app/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { JwtRouter } from "./Jwt.route"; 2 | import { SampleRouter } from "./Sample.route"; 3 | 4 | export { JwtRouter, SampleRouter }; 5 | -------------------------------------------------------------------------------- /app/schemas/index.ts: -------------------------------------------------------------------------------- 1 | import { createSample, deleteSample, updateSample } from "./Sample.schemas"; 2 | 3 | export { createSample, deleteSample, updateSample }; 4 | -------------------------------------------------------------------------------- /app/services/index.ts: -------------------------------------------------------------------------------- 1 | import { JwtService } from "./Jwt.service"; 2 | import { SampleService } from "./Sample.service"; 3 | 4 | export { JwtService, SampleService }; 5 | -------------------------------------------------------------------------------- /app/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import { JWTController } from "./Jwt.controller"; 2 | import { SampleController } from "./Sample.controller"; 3 | 4 | export { JWTController, SampleController }; 5 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["**/*.ts"], 3 | "ext": "ts", 4 | "ignore": ["./test/*.ts"], 5 | "exec": "node -r ts-node/register -r dotenv/config Index.ts", 6 | "env": { 7 | "NODE_ENV": "development" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "no-console": false 9 | }, 10 | "rulesDirectory": [] 11 | } -------------------------------------------------------------------------------- /app/routes/Jwt.route.ts: -------------------------------------------------------------------------------- 1 | import { JWTController } from "../controllers"; 2 | import { Router } from "./Router"; 3 | 4 | export class JwtRouter extends Router { 5 | constructor() { 6 | super(JWTController); 7 | this.router 8 | .post("/", this.handler(JWTController.prototype.index)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/controllers/Controller.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | export abstract class Controller { 4 | 5 | public req: express.Request; 6 | public res: express.Response; 7 | 8 | constructor(req: express.Request, res: express.Response) { 9 | this.req = req; 10 | this.res = res; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/services/Jwt.service.ts: -------------------------------------------------------------------------------- 1 | import * as JWT from "jsonwebtoken"; 2 | import { environment } from "../../env"; 3 | 4 | export class JwtService { 5 | 6 | public signToken(params: { name: string, role: string }, options?: any): string { 7 | return JWT.sign(params, environment.app.secret, options || undefined); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /app/schemas/Sample.schemas.ts: -------------------------------------------------------------------------------- 1 | import { number, object, string } from "@hapi/joi"; 2 | 3 | export const createSample = object().keys({ 4 | text: string().required(), 5 | }); 6 | 7 | export const updateSample = object().keys({ 8 | id: number().required(), 9 | text: string().required(), 10 | }); 11 | 12 | export const deleteSample = object().keys({ 13 | id: number().required(), 14 | }); 15 | -------------------------------------------------------------------------------- /app/models/Sample.model.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail } from "class-validator"; 2 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; 3 | 4 | @Entity("sample") 5 | export class Sample extends BaseEntity { 6 | 7 | @PrimaryGeneratedColumn() 8 | public id: number; 9 | 10 | @Column("text") 11 | public text: string; 12 | 13 | @Column("text") 14 | @IsEmail() 15 | public email: string; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | COPY package*.json ./ 10 | 11 | RUN npm install 12 | # If you are building your code for production 13 | # RUN npm install --only=production 14 | 15 | # Bundle app source 16 | COPY . . 17 | 18 | EXPOSE 1344 19 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /app/routes/Router.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | export abstract class Router { 4 | 5 | public router: express.Router; 6 | private controller: any; 7 | 8 | constructor(controller: any) { 9 | this.controller = controller; 10 | this.router = express.Router(); 11 | } 12 | 13 | protected handler(action: () => void): any { 14 | return (req: Request, res: Response) => action.call(new this.controller(req, res)); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "skipLibCheck": true, 10 | "baseUrl": ".", 11 | "resolveJsonModule": true, 12 | "paths": { 13 | "*": [ 14 | "node_modules/*", 15 | "app/types/*" 16 | ] 17 | } 18 | }, 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /config/Database.ts: -------------------------------------------------------------------------------- 1 | import { createConnection } from "typeorm"; 2 | import { environment } from "../env"; 3 | 4 | export const Connection = createConnection({ 5 | database: environment.db.database, 6 | entities: environment.app.dirs.entities, 7 | host: environment.app.host, 8 | logging: false, 9 | password: environment.db.password, 10 | port: environment.db.port, 11 | synchronize: environment.db.synchronize, 12 | type: environment.db.type as any, 13 | username: environment.db.username, 14 | }); 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs # 2 | /logs 3 | *.log 4 | *.log* 5 | 6 | # Node files # 7 | node_modules/ 8 | npm-debug.log 9 | yarn-error.log 10 | .env 11 | 12 | # Typing # 13 | typings/ 14 | 15 | # Dist # 16 | dist/ 17 | tsconfig.build.json 18 | 19 | # IDE # 20 | .idea/ 21 | *.swp 22 | .awcache 23 | 24 | # Generated source-code # 25 | src/**/*.js 26 | src/**/*.js.map 27 | !src/public/**/* 28 | test/**/*.js 29 | test/**/*.js.map 30 | coverage/ 31 | !test/preprocessor.js 32 | mydb.sql 33 | 34 | # OS generated files # 35 | .DS_Store 36 | /**/.DS_Store 37 | Thumbs.db 38 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 2 | # APPLICATION 3 | # 4 | APP_NAME=restful-starter-server 5 | APP_SCHEMA=http 6 | APP_HOST=localhost 7 | APP_PORT=3000 8 | 9 | APP_SECRET=HltH3R3 10 | 11 | 12 | # 13 | # MySQL DATABASE 14 | # 15 | # mysql or mariadb 16 | TYPEORM_CONNECTION=mariadb 17 | TYPEORM_HOST=localhost 18 | TYPEORM_PORT=3306 19 | TYPEORM_USERNAME=root 20 | TYPEORM_PASSWORD=root 21 | TYPEORM_DATABASE=test 22 | TYPEORM_SYNCHRONIZE=true 23 | TYPEORM_LOGGING=error 24 | TYPEORM_LOGGER=advanced-console 25 | 26 | # 27 | # PATH STRUCTRUE 28 | # 29 | TYPEORM_ENTITIES=app/models/**/*.ts 30 | TYPEORM_ENTITIES_DIR=app/models 31 | -------------------------------------------------------------------------------- /app/controllers/Jwt.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { JwtService } from "../services"; 3 | import { Controller } from "./Controller"; 4 | 5 | export class JWTController extends Controller { 6 | 7 | private jwtService: JwtService; 8 | 9 | constructor(req: Request, res: Response) { 10 | super(req, res); 11 | this.jwtService = new JwtService(); 12 | } 13 | 14 | public async index(): Promise { 15 | const { payload } = this.req.body; 16 | const token = await this.jwtService.signToken(payload); 17 | return this.res.send(token); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /config/Router.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import * as jwt from "express-jwt"; 3 | import { JwtRouter, SampleRouter } from "../app/routes"; 4 | import { environment } from "../env"; 5 | 6 | interface IROUTER { 7 | path: string; 8 | middleware: any[]; 9 | handler: express.Router; 10 | } 11 | 12 | const Sample = new SampleRouter(); 13 | const JWT = new JwtRouter(); 14 | 15 | export const ROUTER: IROUTER[] = [{ 16 | handler: JWT.router, 17 | middleware: [], 18 | path: "/JWT", 19 | }, { 20 | handler: Sample.router, 21 | middleware: [ 22 | jwt({ secret: environment.app.secret }), 23 | ], 24 | path: "/sample", 25 | }, { 26 | handler: Sample.router, 27 | middleware: [], 28 | path: "/", 29 | }]; 30 | -------------------------------------------------------------------------------- /app/middlewares/Validator.ts: -------------------------------------------------------------------------------- 1 | import { ObjectSchema, ValidationOptions } from "@hapi/joi"; 2 | import * as express from "express"; 3 | 4 | const OPTS: ValidationOptions = { 5 | abortEarly: false, 6 | messages: { 7 | key: "{{key}} ", 8 | }, 9 | }; 10 | 11 | export function Validator(schema: ObjectSchema) { 12 | return (req: express.Request, res: express.Response, next: express.NextFunction) => { 13 | const params = req.method === "GET" ? req.params : req.body; 14 | const { error } = schema.validate(params, OPTS); 15 | if (error) { 16 | const { message } = error; 17 | return res.status(400).json({ message }); 18 | } else { 19 | return next(); 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /app/routes/Sample.route.ts: -------------------------------------------------------------------------------- 1 | import { SampleController } from "../controllers"; 2 | import { Validator } from "../middlewares"; 3 | import { createSample, deleteSample, updateSample } from "../schemas"; 4 | import { Router } from "./Router"; 5 | 6 | export class SampleRouter extends Router { 7 | constructor() { 8 | super(SampleController); 9 | this.router 10 | .get("/", this.handler(SampleController.prototype.all)) 11 | .get("/:id", this.handler(SampleController.prototype.find)) 12 | .post("/", [ Validator(createSample) ], this.handler(SampleController.prototype.create)) 13 | .put("/", [ Validator(updateSample) ], this.handler(SampleController.prototype.update)) 14 | .delete("/", [ Validator(deleteSample) ], this.handler(SampleController.prototype.delete)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/repository/Sample.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from "typeorm"; 2 | import { Sample } from "../models"; 3 | 4 | @EntityRepository(Sample) 5 | export class SampleRepository extends Repository { 6 | 7 | public bulkCreate(Samples: Sample[]): Promise { 8 | return this.manager.createQueryBuilder().insert().into(Sample).values(Samples).execute(); 9 | } 10 | 11 | public async removeById(id: number): Promise { 12 | const itemToRemove: Sample = await this.findOne({id}); 13 | return this.manager.remove(itemToRemove); 14 | } 15 | 16 | public findByText(text: string): Promise { 17 | return this.manager.find(Sample, {where: {text}}); 18 | } 19 | 20 | public findOneById(id: number): Promise { 21 | return this.manager.findOne(Sample, {where: {id}}); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Hector Riquelme 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 | -------------------------------------------------------------------------------- /app/services/Sample.service.ts: -------------------------------------------------------------------------------- 1 | import { getCustomRepository } from "typeorm"; 2 | import { Sample } from "../models"; 3 | import { SampleRepository } from "../repository"; 4 | 5 | export class SampleService { 6 | 7 | public findByText(text: string): Promise { 8 | return getCustomRepository(SampleRepository).findByText(text); 9 | } 10 | 11 | public bulkCreate(Samples: Sample[]): Promise { 12 | return getCustomRepository(SampleRepository).bulkCreate(Samples); 13 | } 14 | 15 | public findOneById(id: number): Promise { 16 | return getCustomRepository(SampleRepository).findOneById(id); 17 | } 18 | 19 | public find(): Promise { 20 | return getCustomRepository(SampleRepository).find(); 21 | } 22 | 23 | public remove(sample: Sample): Promise { 24 | return getCustomRepository(SampleRepository).remove(sample); 25 | } 26 | 27 | public removeById(id: number): Promise { 28 | return getCustomRepository(SampleRepository).removeById(id); 29 | } 30 | 31 | public save(sample: Sample): Promise { 32 | return getCustomRepository(SampleRepository).save(sample); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /env.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getOsEnv, 3 | getOsEnvOptional, 4 | getOsPath, 5 | getOsPaths, 6 | normalizePort, 7 | toBool, 8 | toNumber, 9 | } from "./lib/env"; 10 | 11 | /** 12 | * Environment variables 13 | */ 14 | export const environment = { 15 | app: { 16 | dirs: { 17 | entities: getOsPaths("TYPEORM_ENTITIES"), 18 | entitiesDir: getOsPath("TYPEORM_ENTITIES_DIR"), 19 | middlewares: getOsPaths("MIDDLEWARES"), 20 | }, 21 | host: getOsEnv("APP_HOST"), 22 | name: getOsEnv("APP_NAME"), 23 | port: normalizePort(process.env.PORT || getOsEnv("APP_PORT")), 24 | schema: getOsEnv("APP_SCHEMA"), 25 | secret: getOsEnv("APP_SECRET"), 26 | }, 27 | db: { 28 | database: getOsEnv("TYPEORM_DATABASE"), 29 | host: getOsEnvOptional("TYPEORM_HOST"), 30 | logging: getOsEnv("TYPEORM_LOGGING"), 31 | password: getOsEnvOptional("TYPEORM_PASSWORD"), 32 | port: toNumber(getOsEnvOptional("TYPEORM_PORT")), 33 | synchronize: toBool(getOsEnvOptional("TYPEORM_SYNCHRONIZE")), 34 | type: getOsEnv("TYPEORM_CONNECTION"), 35 | username: getOsEnvOptional("TYPEORM_USERNAME"), 36 | }, 37 | isDevelopment: process.env.NODE_ENV === "DEVELOPMENT", 38 | isProduction: process.env.NODE_ENV === "PRODUCTION", 39 | isTest: process.env.NODE_ENV === "test", 40 | node: process.env.NODE_ENV || "DEVELOPMENT", 41 | }; 42 | -------------------------------------------------------------------------------- /lib/env/utils.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | 3 | export function getOsEnv(key: string): string { 4 | if (typeof process.env[key] === "undefined") { 5 | throw new Error(`Environment variable ${key} is not set.`); 6 | } 7 | 8 | return process.env[key] as string; 9 | } 10 | 11 | export function getOsEnvOptional(key: string): string | undefined { 12 | return process.env[key]; 13 | } 14 | 15 | export function getPath(path: string): string { 16 | return process.env.NODE_ENV === "production" 17 | ? join( 18 | process.cwd(), 19 | path.replace("app/", "dist/").slice(0, -3) + ".js", 20 | ) 21 | : join(process.cwd(), path); 22 | } 23 | 24 | export function getPaths(paths: string[]): string[] { 25 | return paths.map((p) => getPath(p)); 26 | } 27 | 28 | export function getOsPath(key: string): string { 29 | return getPath(getOsEnv(key)); 30 | } 31 | 32 | export function getOsPaths(key: string): string[] { 33 | return getPaths(getOsEnvArray(key)); 34 | } 35 | 36 | export function getOsEnvArray(key: string, delimiter: string = ","): string[] { 37 | return (process.env[key] && process.env[key].split(delimiter)) || []; 38 | } 39 | 40 | export function toNumber(value: string): number { 41 | return parseInt(value, 10); 42 | } 43 | 44 | export function toBool(value: string): boolean { 45 | return value === "true"; 46 | } 47 | 48 | export function normalizePort(port: string): number | string | boolean { 49 | const parsedPort = parseInt(port, 10); 50 | if (isNaN(parsedPort)) { 51 | // named pipe 52 | return port; 53 | } 54 | if (parsedPort >= 0) { 55 | // port number 56 | return parsedPort; 57 | } 58 | return false; 59 | } 60 | -------------------------------------------------------------------------------- /Index.ts: -------------------------------------------------------------------------------- 1 | import * as cluster from "cluster"; 2 | import * as dotenv from "dotenv"; 3 | import { cpus } from "os"; 4 | import { resolve } from "path"; 5 | import { env } from "process"; 6 | import { Server } from "./config/Server"; 7 | import { environment } from "./env"; 8 | 9 | dotenv.config({ path: resolve() + "/.env" }); 10 | 11 | if (cluster.isMaster) { 12 | console.log(`\n -------------------> RUN ${env.NODE_ENV} ENVIRONMENT \n`); 13 | for (const _ of cpus()) { 14 | cluster.fork(); 15 | if (!environment.isProduction) { 16 | break; 17 | } 18 | } 19 | cluster.on("exit", (worker, code, signal) => { 20 | console.log("Worker " + worker.process.pid + " died with code: " + code + ", and signal: " + signal); 21 | console.log("Starting a new worker"); 22 | cluster.fork(); 23 | }); 24 | } else { 25 | const port: number = Number(env.PORT) || Number(environment.app.port) || 3000; 26 | new Server().start().then((server) => { 27 | server.listen(port); 28 | server.on("error", (error: any) => { 29 | if (error.syscall !== "listen") { 30 | throw error; 31 | } 32 | switch (error.code) { 33 | case "EACCES": 34 | console.error("Port requires elevated privileges"); 35 | process.exit(1); 36 | break; 37 | case "EADDRINUSE": 38 | console.error("Port is already in use"); 39 | process.exit(1); 40 | break; 41 | default: 42 | throw error; 43 | } 44 | }); 45 | server.on("listening", () => { 46 | const route = () => `${environment.app.schema}://${environment.app.host}:${port}/`; 47 | console.log(``); 48 | console.log(`Server is running in process ${process.pid} on ${route()}`); 49 | console.log(`To shut it down, press + C at any time.`); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-restful-starter", 3 | "version": "0.1.0", 4 | "description": "kit starter node application", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "npm run build && cd dist && cross-env NODE_ENV=PRODUCTION node -r dotenv/config Index.js", 8 | "dev": "nodemon", 9 | "test": "cross-env NODE_ENV=test mocha test/**/*.ts", 10 | "build": "npm run clean && tsc --outDir dist && cp .env ./dist", 11 | "clean": "rimraf dist" 12 | }, 13 | "keywords": [ 14 | "node", 15 | "express", 16 | "typeorm", 17 | "typescript", 18 | "tslint", 19 | "JWT", 20 | "E2015" 21 | ], 22 | "author": "Hector Riquelme", 23 | "license": "MIT", 24 | "dependencies": { 25 | "@hapi/joi": "^17.1.1", 26 | "body-parser": "^1.19.0", 27 | "class-validator": "^0.10.0", 28 | "cluster": "^0.7.7", 29 | "cors": "^2.8.5", 30 | "dotenv": "^8.2.0", 31 | "express": "^4.17.1", 32 | "express-jwt": "^5.3.1", 33 | "fs-extra": "^9.0.0", 34 | "jsonwebtoken": "^8.5.1", 35 | "mariadb": "^2.4.0", 36 | "method-override": "^3.0.0", 37 | "mime-types": "^2.1.27", 38 | "morgan": "^1.10.0", 39 | "multer": "^1.4.2", 40 | "mysql": "^2.18.1", 41 | "os": "^0.1.1", 42 | "path": "^0.12.7", 43 | "superagent": "^5.2.2", 44 | "token-extractor": "^0.1.6", 45 | "ts-node": "^8.10.2", 46 | "typeorm": "^0.2.25", 47 | "typescript": "^3.6.2" 48 | }, 49 | "devDependencies": { 50 | "@types/chai": "^4.2.11", 51 | "@types/cors": "^2.8.6", 52 | "@types/dotenv": "^6.1.1", 53 | "@types/express": "^4.17.6", 54 | "@types/express-jwt": "0.0.42", 55 | "@types/fs-extra": "^9.0.1", 56 | "@types/hapi__joi": "^17.1.0", 57 | "@types/jsonwebtoken": "^8.5.0", 58 | "@types/method-override": "0.0.31", 59 | "@types/mime-types": "^2.1.0", 60 | "@types/mocha": "^5.2.7", 61 | "@types/morgan": "^1.7.37", 62 | "@types/multer": "^1.4.3", 63 | "@types/node": "^12.7.3", 64 | "@types/rimraf": "3.0.0", 65 | "@types/superagent": "^4.1.7", 66 | "@types/supertest": "^2.0.9", 67 | "chai": "^4.2.0", 68 | "cross-env": "^7.0.2", 69 | "mocha": "^6.2.0", 70 | "nodemon": "^2.0.4", 71 | "rimraf": "^3.0.2", 72 | "supertest": "^4.0.2", 73 | "tslint": "^5.19.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typescript-restful-starter 2 | Node.js + ExpressJS + TypeOrm + Typescript + JWT + ES2015 + Clustering + Tslint + Mocha + Chai + Supertest 3 | ------------ 4 | # What use is this Starter App? 5 | - **JWT** for protecting routes. 6 | - **Clustering mode** for loading many forks depending of the CPU's units. 7 | - **Typeorm** for ORM. 8 | - **ES2015** the lastest javascript version has promises and async/await 9 | - **Mocha - Chai** for testing 10 | - **Supertest** to load the entire server into the tests seamlessly 11 | 12 | ## Structure 13 | ```json 14 | /app 15 | /controllers (Controllers of the app) 16 | /middlewares (Middlewares for the routes of the app) 17 | /routes (Routes for Controllers of the app) 18 | /service (Services for using in any Controller) 19 | /entity (Models configuration for use) 20 | /repository (Custom queries) 21 | /config 22 | /Router.ts (Config file for Routing) 23 | /Database (DB configuration for use) 24 | /Server.ts (Server configuration) 25 | config.ts (Config file for the app) 26 | tsconfig.json (File configuration typescript) 27 | tslint.json (File configuration rules typescript) 28 | Index.ts (Main file to start the app) 29 | ``` 30 | # Install 31 | 1. First clone this repository. 32 | 33 | git@github.com:camesine/Typescript-restful-starter.git 34 | 35 | 2. Download all dependencies. 36 | 37 | npm install 38 | 39 | 3. Edit the file `./env` and add config database like: 40 | 41 | ```js 42 | # 43 | # APPLICATION 44 | # 45 | APP_NAME=restful-starter-server 46 | APP_SCHEMA=http 47 | APP_HOST=localhost 48 | APP_PORT=3000 49 | 50 | APP_SECRET=HltH3R3 51 | 52 | 53 | # 54 | # MySQL DATABASE 55 | # 56 | # mysql or mariadb 57 | TYPEORM_CONNECTION=mariadb 58 | TYPEORM_HOST=localhost 59 | TYPEORM_PORT=3306 60 | TYPEORM_USERNAME=root 61 | TYPEORM_PASSWORD=root 62 | TYPEORM_DATABASE=test 63 | TYPEORM_SYNCHRONIZE=true 64 | TYPEORM_LOGGING=error 65 | TYPEORM_LOGGER=advanced-console 66 | 67 | # 68 | # PATH STRUCTRUE 69 | # 70 | TYPEORM_ENTITIES=app/models/**/*.ts 71 | TYPEORM_ENTITIES_DIR=app/models 72 | ``` 73 | 74 | # Start App 75 | When execute any of this commands the app start with clustering, creating many cluster apps depending of the numbers of CPU's your computer had. 76 | ### Development: In Development mode, the express app is started with nodemon for automatic refresh when changes are made. 77 | npm run dev 78 | ### Test: Run test in development environment 79 | npm test 80 | ### Production: Run app in production environment 81 | npm start 82 | -------------------------------------------------------------------------------- /app/controllers/Sample.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { Sample } from "../models"; 3 | import { SampleService } from "../services"; 4 | import { Controller } from "./Controller"; 5 | 6 | export class SampleController extends Controller { 7 | 8 | private sampleService: SampleService; 9 | private sample: Sample; 10 | 11 | constructor(req: Request, res: Response) { 12 | super(req, res); 13 | this.sample = new Sample(); 14 | this.sampleService = new SampleService(); 15 | } 16 | 17 | public async all(): Promise { 18 | const sampleList = await this.sampleService.find(); 19 | return this.res.send(sampleList); 20 | } 21 | 22 | public async find(): Promise { 23 | const { id } = this.req.params as unknown as { id: number }; 24 | const sample = await this.sampleService.findOneById(id); 25 | if (sample) { 26 | return this.res.status(200).send(sample); 27 | } else { 28 | return this.res.status(404).send({ text: "not found" }); 29 | } 30 | } 31 | 32 | public async create(): Promise { 33 | const { text } = this.req.body as { text: string }; 34 | this.sample.text = text; 35 | this.sample.email = "someone@somewhere.com"; 36 | try { 37 | const result = await this.sampleService.save(this.sample); 38 | return this.res.status(200).send(result); 39 | } catch (ex) { 40 | return this.res.status(404).send({ text: "ERROR" }); 41 | } 42 | } 43 | 44 | public async update(): Promise { 45 | const { id, text, email } = this.req.body as { id: number, text: string, email: string }; 46 | this.sample.id = id; 47 | this.sample.text = text; 48 | this.sample.email = email; 49 | try { 50 | const sample = await this.sampleService.save(this.sample); 51 | if (sample) { 52 | return this.res.status(200).send(); 53 | } else { 54 | return this.res.status(404).send({ text: "not found" }); 55 | } 56 | } catch (ex) { 57 | return this.res.status(404).send({ text: "error" }); 58 | } 59 | } 60 | 61 | public async delete(): Promise { 62 | const { id } = this.req.body as { id: number }; 63 | try { 64 | await this.sampleService.removeById(id); 65 | return this.res.status(204).send(); 66 | } catch (ex) { 67 | return this.res.status(404).send({ text: "ERROR" }); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /config/Server.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from "body-parser"; 2 | import * as cors from "cors"; 3 | import * as express from "express"; 4 | import * as http from "http"; 5 | import * as methodOverride from "method-override"; 6 | import * as morgan from "morgan"; 7 | import { Connection } from "./Database"; 8 | import { ROUTER } from "./Router"; 9 | 10 | export class Server { 11 | 12 | private static connectDB(): Promise { 13 | return Connection; 14 | } 15 | 16 | private readonly app: express.Application; 17 | private readonly server: http.Server; 18 | 19 | constructor() { 20 | this.app = express(); 21 | this.server = http.createServer(this.app); 22 | } 23 | 24 | public async start(): Promise { 25 | await Server.connectDB(); 26 | this.expressConfiguration(); 27 | this.configurationRouter(); 28 | return this.server; 29 | } 30 | 31 | public App(): express.Application { 32 | return this.app; 33 | } 34 | 35 | private expressConfiguration(): void { 36 | this.app.use(bodyParser.urlencoded({ extended: true })); 37 | this.app.use(bodyParser.json({ limit: "50mb" })); 38 | this.app.use(methodOverride()); 39 | this.app.use((req, res, next): void => { 40 | res.header("Access-Control-Allow-Origin", "*"); 41 | res.header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization"); 42 | res.header("Access-Control-Allow-Methods", "GET,PUT,PATCH,POST,DELETE,OPTIONS"); 43 | next(); 44 | }); 45 | this.app.use(morgan("combined")); 46 | this.app.use(cors()); 47 | this.app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction): void => { 48 | err.status = 404; 49 | next(err); 50 | }); 51 | } 52 | 53 | private configurationRouter(): void { 54 | for (const route of ROUTER) { 55 | this.app.use(route.path, route.middleware, route.handler); 56 | } 57 | this.app.use((req: express.Request, res: express.Response, next: express.NextFunction): void => { 58 | res.status(404); 59 | res.json({ 60 | error: "Not found", 61 | }); 62 | next(); 63 | }); 64 | this.app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction): void => { 65 | if (err.name === "UnauthorizedError") { 66 | res.status(401).json({ 67 | error: "Please send a valid Token...", 68 | }); 69 | } 70 | next(); 71 | }); 72 | this.app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction): void => { 73 | res.status(err.status || 500); 74 | res.json({ 75 | error: err.message, 76 | }); 77 | next(); 78 | }); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /test/Sample.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as dotenv from "dotenv"; 3 | import * as express from "express"; 4 | import { resolve } from "path"; 5 | import * as supertest from "supertest"; 6 | import { Sample } from "../app/models"; 7 | import { JwtService } from "../app/services/Jwt.service"; 8 | import { SampleService } from "../app/services/Sample.service"; 9 | import { Server } from "../config/Server"; 10 | 11 | dotenv.config({ path: resolve() + "/.env" }); 12 | 13 | let token: string; 14 | let IdRecord: number; 15 | let IdRecordTwo: number; 16 | const server: Server = new Server(); 17 | let app: express.Application; 18 | 19 | const sampleService = new SampleService(); 20 | 21 | describe("Sample route", () => { 22 | 23 | before((done) => { 24 | const sample = new Sample(); 25 | sample.text = "SAMPLE TEXT"; 26 | sample.email = "someone@somewhere.com"; 27 | server.start().then(() => { 28 | app = server.App(); 29 | Promise.all([ 30 | new JwtService().signToken({ name: "name", role: "rol" }), 31 | sampleService.save(sample), 32 | ]).then((res) => { 33 | token = res[0]; 34 | IdRecord = res[1].id; 35 | done(); 36 | }); 37 | }); 38 | }); 39 | 40 | after(async () => { 41 | const sampleOne = await sampleService.findOneById(IdRecord); 42 | const sampleTwo = await sampleService.findOneById(IdRecordTwo); 43 | if (sampleOne) { 44 | await sampleService.remove(sampleOne); 45 | } 46 | if (sampleTwo) { 47 | await sampleService.remove(sampleTwo); 48 | } 49 | }); 50 | 51 | it("Random Url gives 404", (done) => { 52 | supertest(app).get("/random-url") 53 | .set("Authorization", `bearer ${token}`).set("Accept", "application/json") 54 | .end((err: Error, res: supertest.Response) => { 55 | chai.expect(res.status).to.be.a("number"); 56 | chai.expect(res.status).to.eq(404); 57 | done(); 58 | }); 59 | }); 60 | 61 | it("Can list all Samples", (done) => { 62 | supertest(app).get("/") 63 | .set("Authorization", `bearer ${token}`).set("Accept", "application/json") 64 | .end((err: Error, res: supertest.Response) => { 65 | chai.expect(res.status).to.be.a("number"); 66 | chai.expect(res.status).to.eq(200); 67 | chai.expect(res.body).to.be.a("array"); 68 | chai.expect(res.body[0].text).to.be.a("string"); 69 | done(); 70 | }); 71 | }); 72 | 73 | it("Can search for Sample by Id", (done) => { 74 | supertest(app).get(`/${IdRecord}`) 75 | .set("Authorization", `bearer ${token}`).set("Accept", "application/json") 76 | .end((err: Error, res: supertest.Response) => { 77 | chai.expect(res.status).to.eq(200); 78 | chai.expect(res.body).to.be.a("object"); 79 | chai.expect(res.body).to.have.all.keys("id", "text", "email"); 80 | chai.expect(res.body.text).to.be.a("string"); 81 | done(); 82 | }); 83 | }); 84 | 85 | it("Can create a new Sample", (done) => { 86 | supertest(app).post("/") 87 | .set("Authorization", `bearer ${token}`) 88 | .set("Accept", "application/json") 89 | .send({text: "Sample text 100"}) 90 | .end((err: Error, res: supertest.Response) => { 91 | chai.expect(res.status).to.eq(200); 92 | chai.expect(res.body).to.have.all.keys("id", "text", "email"); 93 | chai.expect(res.body.id).to.be.a("number"); 94 | chai.expect(res.body.text).to.be.a("string"); 95 | IdRecordTwo = res.body.id; 96 | done(); 97 | }); 98 | }); 99 | 100 | it("Can update an existing Sample", (done) => { 101 | supertest(app).put("/") 102 | .set("Authorization", `bearer ${token}`) 103 | .set("Accept", "application/json") 104 | .send({id: IdRecord, text: "Sample text updateado"}) 105 | .end((err: Error, res: supertest.Response) => { 106 | chai.expect(res.status).to.eq(200); 107 | done(); 108 | }); 109 | }); 110 | 111 | it("Can remove a sample by Id", (done) => { 112 | supertest(app).delete("/").set("Authorization", `bearer ${token}`) 113 | .set("Accept", "application/json") 114 | .send({id: IdRecord}) 115 | .end((err: Error, res: supertest.Response) => { 116 | chai.expect(res.status).to.eq(204); 117 | done(); 118 | }); 119 | }); 120 | 121 | it("Reports an error when finding a non-existent Sample by Id", (done) => { 122 | supertest(app).get(`/9999`) 123 | .set("Authorization", `bearer ${token}`) 124 | .set("Accept", "application/json") 125 | .end((err: Error, res: supertest.Response) => { 126 | chai.expect(res.status).to.eq(404); 127 | chai.expect(res.body).to.have.all.keys("text"); 128 | chai.expect(res.body.text).to.be.a("string"); 129 | chai.expect(res.body.text).to.equal("not found"); 130 | done(); 131 | }); 132 | }); 133 | 134 | it("Reports an error when trying to create an invalid Sample", (done) => { 135 | supertest(app).post("/").set("Authorization", `bearer ${token}`) 136 | .set("Accept", "application/json") 137 | .send({sample: "XXXX"}) 138 | .end((err: Error, res: supertest.Response) => { 139 | chai.expect(res.status).to.eq(400); 140 | done(); 141 | }); 142 | }); 143 | 144 | it("Reports an error when trying to update a Sample with invalid data", (done) => { 145 | supertest(app).put("/").set("Authorization", `bearer ${token}`) 146 | .set("Accept", "application/json") 147 | .send({sample: "XXXX"}) 148 | .end((err: Error, res: supertest.Response) => { 149 | chai.expect(res.status).to.eq(400); 150 | done(); 151 | }); 152 | }); 153 | 154 | it("Reports an error when trying to delete a Sample with invalid data", (done) => { 155 | supertest(app).delete("/").set("Authorization", `bearer ${token}`) 156 | .set("Accept", "application/json") 157 | .send({sample: "XXXX"}) 158 | .end((err: Error, res: supertest.Response) => { 159 | chai.expect(res.status).to.eq(400); 160 | done(); 161 | }); 162 | }); 163 | 164 | }); 165 | --------------------------------------------------------------------------------