├── .gitignore ├── .npmignore ├── .npmrc ├── Jenkinsfile ├── Readme.md ├── configuration ├── dev │ ├── cm │ │ └── variables.properties │ └── variables.properties ├── postman │ ├── dev.json │ ├── postman_collection.json │ ├── prod.json │ ├── test.json │ └── uat.json ├── prod │ ├── cm │ │ └── variables.properties │ └── variables.properties ├── test │ ├── cm │ │ └── variables.properties │ └── variables.properties └── uat │ ├── cm │ └── variables.properties │ └── variables.properties ├── openshift ├── pipeline │ ├── JenkinsFile │ └── Readme.md └── templates │ └── nodejs-app.json ├── package-lock.json ├── package.json ├── scripts ├── updateCode.js └── updateSwagger.js ├── src ├── application.ts ├── controllers │ └── hello.ts ├── cors.ts ├── definition │ └── swagger.yaml ├── env.ts ├── index.ts ├── lib │ ├── .gitkeep │ ├── asyncHandler.ts │ └── cls.ts ├── log.ts ├── middlewares │ └── swagger.ts ├── monitoring │ ├── index.ts │ ├── init.ts │ └── mw.ts ├── routes │ └── helloWorldRoute.ts └── typings.d.ts ├── test ├── application.spec.ts └── routes │ └── helloWorldRoute.spec.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dump.rdb 4 | *~ 5 | cov-* 6 | coverage 7 | plato 8 | .idea 9 | .DS_Store 10 | *.sw* 11 | launch.json 12 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = "http://nexus3-misanche-nexus.apps.na39.openshift.opentlc.com/repository/npm_group" 2 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | @Library('pipelines') _ 2 | 3 | npmFullPipeline( 4 | appName: params.APP_NAME, 5 | gitBranch: params.GIT_BRANCH, 6 | gitCredentials: params.GIT_CREDENTIALS, 7 | gitUrl: params.GIT_URL, 8 | buildProject: params.BUILD_PROJECT, 9 | uatProject: params.UAT_PROJECT, 10 | prodProject: params.PROD_PROJECT, 11 | baseImage: params.BASE_IMAGE, 12 | buildTag: params.BUILD_TAG, 13 | deployTag: params.DEPLOY_TAG, 14 | testStrategy: params.STRATEGY 15 | ) -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Nodejs FES Template 2 | 3 | # Environment vars 4 | This project uses the following environment variables: 5 | 6 | | Name | Description | Default Value | 7 | | ----------------------------- | ------------------------------------| -----------------------------------------------| 8 | |CORS | Cors accepted values | "*"   | 9 | 10 | 11 | # Pre-requisites 12 | - Install [Node.js](https://nodejs.org/en/) version 8.0.0 13 | 14 | 15 | # Getting started 16 | - Clone the repository 17 | ``` 18 | git clone 19 | ``` 20 | - Install dependencies 21 | ``` 22 | cd 23 | npm install 24 | ``` 25 | - Build and run the project 26 | ``` 27 | npm start 28 | ``` 29 | Navigate to `http://localhost:8001` 30 | 31 | - API Document endpoints 32 | 33 | swagger Spec Endpoint : http://localhost:8001/api-docs 34 | 35 | swagger-ui Endpoint : http://localhost:8001/docs 36 | 37 | 38 | # TypeScript + Node 39 | The main purpose of this repository is to show a project setup and workflow for writing microservice. The Rest APIs will be using the Swagger (OpenAPI) Specification. 40 | 41 | 42 | 43 | 44 | ## Getting TypeScript 45 | Add Typescript to project `npm`. 46 | ``` 47 | npm install -D typescript 48 | ``` 49 | 50 | ## Project Structure 51 | The folder structure of this app is explained below: 52 | 53 | | Name | Description | 54 | | ------------------------ | --------------------------------------------------------------------------------------------- | 55 | | **dist** | Contains the distributable (or output) from your TypeScript build. | 56 | | **node_modules** | Contains all npm dependencies | 57 | | **src** | Contains source code that will be compiled to the dist dir | 58 | | **configuration** | Application configuration including environment-specific configs 59 | | **src/controllers** | Controllers define functions to serve various express routes. 60 | | **src/lib** | Common libraries to be used across your app. 61 | | **src/middlewares** | Express middlewares which process the incoming requests before handling them down to the routes 62 | | **src/routes** | Contain all express routes, separated by module/area of application 63 | | **src/models** | Models define schemas that will be used in storing and retrieving data from Application database | 64 | | **src/monitoring** | Prometheus metrics | 65 | | **src**/index.ts | Entry point to express app | 66 | | package.json | Contains npm dependencies as well as [build scripts](#what-if-a-library-isnt-on-definitelytyped) | tsconfig.json | Config settings for compiling source code only written in TypeScript 67 | | tslint.json | Config settings for TSLint code style checking | 68 | 69 | ## Building the project 70 | ### Configuring TypeScript compilation 71 | ```json 72 | { 73 | "compilerOptions": { 74 | "target": "es5", 75 | "module": "commonjs", 76 | "outDir": "dist", 77 | "sourceMap": true 78 | }, 79 | 80 | "include": [ 81 | "src/**/*.ts" 82 | 83 | 84 | ], 85 | "exclude": [ 86 | "src/**/*.spec.ts", 87 | "test", 88 | "node_modules" 89 | 90 | ] 91 | } 92 | 93 | ``` 94 | 95 | ### Running the build 96 | All the different build steps are orchestrated via [npm scripts](https://docs.npmjs.com/misc/scripts). 97 | Npm scripts basically allow us to call (and chain) terminal commands via npm. 98 | 99 | | Npm Script | Description | 100 | | ------------------------- | ------------------------------------------------------------------------------------------------- | 101 | | `start` | Runs full build and runs node on dist/index.js. Can be invoked with `npm start` | 102 | | `build:copy` | copy the *.yaml file to dist/ folder | 103 | | `build:live` | Full build. Runs ALL build tasks | 104 | | `build:dev` | Full build. Runs ALL build tasks with all watch tasks | 105 | | `dev` | Runs full build before starting all watch tasks. Can be invoked with `npm dev` | 106 | | `test` | Runs build and run tests using mocha | 107 | | `lint` | Runs TSLint on project files | 108 | 109 | ### Using the debugger in VS Code 110 | Node.js debugging in VS Code is easy to setup and even easier to use. 111 | Press `F5` in VS Code, it looks for a top level `.vscode` folder with a `launch.json` file. 112 | 113 | ```json 114 | { 115 | "version": "0.2.0", 116 | "configurations": [ 117 | { 118 | "type": "node", 119 | "request": "launch", 120 | "name": "Launch Program", 121 | "program": "${workspaceFolder}/dist/index.js", 122 | "preLaunchTask": "tsc: build - tsconfig.json", 123 | 124 | "outFiles": [ 125 | "${workspaceFolder}/dist/*js" 126 | ] 127 | }, 128 | 129 | { 130 | // Name of configuration; appears in the launch configuration drop down menu. 131 | "name": "Run mocha", 132 | "request":"launch", 133 | // Type of configuration. Possible values: "node", "mono". 134 | "type": "node", 135 | // Workspace relative or absolute path to the program. 136 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 137 | 138 | // Automatically stop program after launch. 139 | "stopOnEntry": false, 140 | // Command line arguments passed to the program. 141 | "args": ["--no-timeouts", "--compilers", "ts:ts-node/register", "${workspaceRoot}/test/*"], 142 | 143 | // Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace. 144 | 145 | // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. 146 | "runtimeExecutable": null, 147 | // Environment variables passed to the program. 148 | "env": { "NODE_ENV": "test"} 149 | } 150 | ] 151 | } 152 | ``` 153 | 154 | ## Testing 155 | The tests are written in Mocha and the assertions done using Chai 156 | 157 | ``` 158 | "mocha": "3.4.2", 159 | "chai": "4.1.2", 160 | "chai-http": "3.0.0", 161 | 162 | ``` 163 | 164 | ### Example application.spec.ts 165 | ``` 166 | import chaiHttp = require("chai-http") 167 | import * as chai from "chai" 168 | import app from './application' 169 | 170 | const expect = chai.expect; 171 | chai.use(chaiHttp); 172 | 173 | 174 | describe('App', () => { 175 | it('works', (done:Function): void => { 176 | chai.request(app) 177 | .get('/api/hello?greeting=world') 178 | .send({}) 179 | .end((err:Error, res: any): void => { 180 | 181 | expect(res.statusCode).to.be.equal(200); 182 | expect(res.body.msg).to.be.equal("hello world"); 183 | done(); 184 | }); 185 | 186 | }); 187 | }); 188 | ``` 189 | ### Running tests using NPM Scripts 190 | ```` 191 | npm run test 192 | 193 | ```` 194 | Test files are created under test folder. 195 | 196 | 197 | # Swagger 198 | ## Specification 199 | The swagger specification file is named as swagger.yaml. The file is located under definition folder. 200 | Example: 201 | ``` 202 | paths: 203 | /hello: 204 | get: 205 | x-swagger-router-controller: helloWorldRoute 206 | operationId: helloWorldGet 207 | tags: 208 | - /hello 209 | description: >- 210 | Returns the current weather for the requested location using the 211 | requested unit. 212 | parameters: 213 | - name: greeting 214 | in: query 215 | description: Name of greeting 216 | required: true 217 | type: string 218 | responses: 219 | '200': 220 | description: Successful request. 221 | schema: 222 | $ref: '#/definitions/Hello' 223 | default: 224 | description: Invalid request. 225 | schema: 226 | $ref: '#/definitions/Error' 227 | definitions: 228 | Hello: 229 | properties: 230 | msg: 231 | type: string 232 | required: 233 | - msg 234 | Error: 235 | properties: 236 | message: 237 | type: string 238 | required: 239 | - message 240 | ``` 241 | ### Highlights of the swagger.yaml File 242 | 243 | - /hello: 244 | 245 | Specifies how users should be routed when they make a request to this endpoint. 246 | - x-swagger-router-controller: helloWorldRoute 247 | 248 | Specifies which code file acts as the controller for this endpoint. 249 | - get: 250 | 251 | Specifies the method being requested (GET, PUT, POST, etc.). 252 | - operationId: hello 253 | 254 | Specifies the direct method to invoke for this endpoint within the controller/router 255 | - parameters: 256 | 257 | This section defines the parameters of your endpoint. They can be defined as path, query, header, formData, or body. 258 | - definitions: 259 | 260 | This section defines the structure of objects used in responses or as parameters. 261 | ## Swagger Middleware 262 | The project is using npm module `swagger-tools` that provides middleware functions for metadata, security, validation and routing, and bundles Swagger UI into Express. 263 | ``` 264 | swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) { 265 | // Interpret Swagger resources and attach metadata to request - must be first in swagger-tools middleware chain 266 | app.use(middleware.swaggerMetadata()); 267 | 268 | // Validate Swagger requests 269 | app.use(middleware.swaggerValidator({})); 270 | 271 | // Route validated requests to appropriate controller 272 | app.use(middleware.swaggerRouter(options)); 273 | 274 | // Serve the Swagger documents and Swagger UI 275 | app.use(middleware.swaggerUi()); 276 | cb(); 277 | 278 | }) 279 | ``` 280 | - Metadata 281 | 282 | Swagger extends the Express request object, so that each route handler has access to incoming parameters that have been parsed based on the spec, as well as additional Swagger-generated information from the client. 283 | 284 | Any incoming parameters for the API call will be available in `req.swagger` regardless of whether they were transmitted using query, body, header, etc. 285 | 286 | - Validator 287 | 288 | Validation middleware will only route requests that match paths in Swagger specification exactly in terms of endpoint path, request mime type, required and optional parameters, and their declared types. 289 | 290 | - Swagger Router 291 | 292 | The Swagger Router connects the Express route handlers found in the controller files on the path specified, with the paths defined in the Swagger specification (swagger.yaml). The routing looks up the correct controller file and exported function based on parameters added to the Swagger spec for each path. 293 | 294 | Here is an example for a hello world endpoint: 295 | 296 | ``` 297 | paths: 298 | /hello: 299 | get: 300 | x-swagger-router-controller: helloWorldRoute 301 | operationId: helloWorldGet 302 | tags: 303 | - /hello 304 | description: >- 305 | Returns the current weather for the requested location using the 306 | requested unit. 307 | parameters: 308 | - name: greeting 309 | in: query 310 | description: Name of greeting 311 | required: true 312 | type: string 313 | responses: 314 | '200': 315 | description: Successful request. 316 | schema: 317 | $ref: '#/definitions/Hello' 318 | default: 319 | description: Invalid request. 320 | schema: 321 | $ref: '#/definitions/Error' 322 | ``` 323 | The fields `x-swagger-router-controller` will point the middleware to a `helloWorldRoute.ts` file in the route's directory, while the `operationId` names the handler function to be invoked. 324 | 325 | - Swagger UI 326 | 327 | The final piece of middleware enables serving of the swagger-ui interface direct from the Express server. It also serves the raw Swagger schema (.json) that clients can consume. Paths for both are configurable. 328 | The swagger-ui endpoint is acessible at /docs endpoint. 329 | 330 | # TSLint 331 | TSLint is a code linter that helps catch minor code quality and style issues. 332 | 333 | ## TSLint rules 334 | All rules are configured through `tslint.json`. 335 | 336 | 337 | ## Running TSLint 338 | To run TSLint you can call the main build script or just the TSLint task. 339 | ``` 340 | npm run build:live // runs full build including TSLint 341 | npm run lint // runs only TSLint 342 | ``` 343 | 344 | 345 | # Common Issues 346 | 347 | ## npm install fails 348 | The current solution has an example for using a private npm repository. if you want to use the public npm repository, remove the .npmrc file. 349 | 350 | 351 | -------------------------------------------------------------------------------- /configuration/dev/cm/variables.properties: -------------------------------------------------------------------------------- 1 | OPENSHIFT_NODEJS_PORT=8080 -------------------------------------------------------------------------------- /configuration/dev/variables.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhappdev/nodejs-template/6bdcd1fb52c22857848126195f09e7318dc9396f/configuration/dev/variables.properties -------------------------------------------------------------------------------- /configuration/postman/dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4d531526-66a5-4de9-9f2b-9ea4df766907", 3 | "name": "dev", 4 | "values": [{ 5 | "key": "HOST", 6 | "value": "http://nodejs-misanchez-dev.apps.na39.openshift.opentlc.com", 7 | "type": "text", 8 | "description": "", 9 | "enabled": true 10 | }], 11 | "_postman_variable_scope": "environment", 12 | "_postman_exported_at": "2018-10-11T08:37:30.705Z", 13 | "_postman_exported_using": "Postman/6.3.0" 14 | } -------------------------------------------------------------------------------- /configuration/postman/postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "f7f195bc-65cc-47a8-bb7a-6e288186b0d3", 4 | "name": "Simple Weather API", 5 | "description": "API for getting the current weather information.", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 7 | }, 8 | "item": [ 9 | { 10 | "name": "hello", 11 | "item": [ 12 | { 13 | "name": "", 14 | "event": [ 15 | { 16 | "listen": "test", 17 | "script": { 18 | "id": "6822494f-d81d-4522-8d31-4912adf9cf00", 19 | "type": "text/javascript", 20 | "exec": [ 21 | "pm.test(\"Status code is 200\", function () {", 22 | " pm.response.to.have.status(200);", 23 | "});" 24 | ] 25 | } 26 | } 27 | ], 28 | "request": { 29 | "method": "GET", 30 | "header": [ 31 | { 32 | "key": "Accept", 33 | "value": "application/json" 34 | } 35 | ], 36 | "body": {}, 37 | "url": { 38 | "raw": "{{HOST}}/api/hello?greeting=Mikel", 39 | "host": [ 40 | "{{HOST}}" 41 | ], 42 | "path": [ 43 | "api", 44 | "hello" 45 | ], 46 | "query": [ 47 | { 48 | "key": "greeting", 49 | "value": "Mikel" 50 | } 51 | ] 52 | }, 53 | "description": "Returns the current weather for the requested location using the requested unit." 54 | }, 55 | "response": [] 56 | }, 57 | { 58 | "name": "", 59 | "event": [ 60 | { 61 | "listen": "test", 62 | "script": { 63 | "id": "a24b228c-442b-4ffa-bced-02b200ddc856", 64 | "type": "text/javascript", 65 | "exec": [ 66 | "pm.test(\"Status code is 200\", function () {", 67 | " pm.response.to.have.status(200);", 68 | "});", 69 | "", 70 | "pm.test(\"Status code is 200\", function () {", 71 | " pm.response.to.have.status(200);", 72 | "});" 73 | ] 74 | } 75 | } 76 | ], 77 | "request": { 78 | "method": "POST", 79 | "header": [ 80 | { 81 | "key": "Accept", 82 | "value": "application/json" 83 | } 84 | ], 85 | "body": {}, 86 | "url": { 87 | "raw": "{{HOST}}/api/hello?greeting=Mikel", 88 | "host": [ 89 | "{{HOST}}" 90 | ], 91 | "path": [ 92 | "api", 93 | "hello" 94 | ], 95 | "query": [ 96 | { 97 | "key": "greeting", 98 | "value": "Mikel" 99 | } 100 | ] 101 | }, 102 | "description": "Returns the current weather for the requested location using the requested unit." 103 | }, 104 | "response": [] 105 | } 106 | ], 107 | "description": "Folder for hello" 108 | } 109 | ] 110 | } -------------------------------------------------------------------------------- /configuration/postman/prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ed5a6095-889f-478c-b057-22a820c83e18", 3 | "name": "prod", 4 | "values": [{ 5 | "key": "HOST", 6 | "value": "http://nodejs-misanchez-prod.apps.na39.openshift.opentlc.com", 7 | "description": "", 8 | "enabled": true 9 | }], 10 | "_postman_variable_scope": "environment", 11 | "_postman_exported_at": "2018-10-11T08:37:56.527Z", 12 | "_postman_exported_using": "Postman/6.3.0" 13 | } -------------------------------------------------------------------------------- /configuration/postman/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "49d3b6e7-c82d-4b08-a700-db85a4b77caf", 3 | "name": "test", 4 | "values": [{ 5 | "key": "HOST", 6 | "value": "http://nodejs-misanchez-test.apps.na39.openshift.opentlc.com", 7 | "description": "", 8 | "enabled": true 9 | }], 10 | "_postman_variable_scope": "environment", 11 | "_postman_exported_at": "2018-10-11T08:38:07.251Z", 12 | "_postman_exported_using": "Postman/6.3.0" 13 | } -------------------------------------------------------------------------------- /configuration/postman/uat.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0a1b02fa-7575-4a30-8bfc-154c990e45d1", 3 | "name": "uat", 4 | "values": [{ 5 | "key": "HOST", 6 | "value": "http://nodejs-misanchez-uat.apps.na39.openshift.opentlc.com", 7 | "description": "", 8 | "enabled": true 9 | }], 10 | "_postman_variable_scope": "environment", 11 | "_postman_exported_at": "2018-10-11T08:38:13.329Z", 12 | "_postman_exported_using": "Postman/6.3.0" 13 | } -------------------------------------------------------------------------------- /configuration/prod/cm/variables.properties: -------------------------------------------------------------------------------- 1 | OPENSHIFT_NODEJS_PORT=8080 -------------------------------------------------------------------------------- /configuration/prod/variables.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhappdev/nodejs-template/6bdcd1fb52c22857848126195f09e7318dc9396f/configuration/prod/variables.properties -------------------------------------------------------------------------------- /configuration/test/cm/variables.properties: -------------------------------------------------------------------------------- 1 | OPENSHIFT_NODEJS_PORT=8080 -------------------------------------------------------------------------------- /configuration/test/variables.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhappdev/nodejs-template/6bdcd1fb52c22857848126195f09e7318dc9396f/configuration/test/variables.properties -------------------------------------------------------------------------------- /configuration/uat/cm/variables.properties: -------------------------------------------------------------------------------- 1 | OPENSHIFT_NODEJS_PORT=8080 -------------------------------------------------------------------------------- /configuration/uat/variables.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhappdev/nodejs-template/6bdcd1fb52c22857848126195f09e7318dc9396f/configuration/uat/variables.properties -------------------------------------------------------------------------------- /openshift/pipeline/JenkinsFile: -------------------------------------------------------------------------------- 1 | node('nodejs') { 2 | stage 'build' 3 | openshiftBuild(namespace: 'prima-poc' ,buildConfig: 'nodejssample', showBuildLogs: 'true') 4 | stage 'deploy' 5 | openshiftDeploy(namespace: 'prima-poc',deploymentConfig: 'nodejssample') 6 | openshiftScale(namespace: 'prima-poc',deploymentConfig: 'nodejssample',replicaCount: '1') 7 | stage 'deployInTesting' 8 | openshiftTag(namespace: 'prima-poc', sourceStream: 'nodejssample', sourceTag: 'latest', destinationStream: 'nodejssample', destinationTag: 'promoteToQA') 9 | openshiftDeploy(namespace: 'test-project', deploymentConfig: 'nodejssample', ) 10 | openshiftScale(namespace: 'test-project', deploymentConfig: 'nodejssample',replicaCount: '1') 11 | } -------------------------------------------------------------------------------- /openshift/pipeline/Readme.md: -------------------------------------------------------------------------------- 1 | This directory contains a Jenkinsfile which can be used to build 2 | nodejsApp using an OpenShift build pipeline. 3 | 4 | To do this, run: 5 | 6 | ```bash 7 | 8 | # Pipeline should be create under jenkins project 9 | # create the pipeline build controller from the openshift/pipeline 10 | # subdirectory 11 | oc project <> 12 | oc new-app < \ 13 | --context-dir=openshift/pipeline --name <> -------------------------------------------------------------------------------- /openshift/templates/nodejs-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "Template", 3 | "apiVersion": "v1", 4 | "metadata": { 5 | "name": "nodejs-app", 6 | "annotations": { 7 | "openshift.io/display-name": "Node.js", 8 | "description": "An example Node.js application with no database. For more information about using this template, including OpenShift considerations, see https://github.com/openshift/nodejs-ex/blob/master/README.md.", 9 | "tags": "quickstart,nodejs", 10 | "iconClass": "icon-nodejs", 11 | "openshift.io/long-description": "This template defines resources needed to develop a NodeJS application, including a build configuration and application deployment configuration. It does not include a database.", 12 | "openshift.io/provider-display-name": "Red Hat, Inc.", 13 | "openshift.io/documentation-url": "https://github.com/openshift/nodejs-ex", 14 | "openshift.io/support-url": "https://access.redhat.com" 15 | } 16 | }, 17 | "message": "The following service(s) have been created in your project: ${NAME}.\n\nFor more information about using this template, including OpenShift considerations, see https://github.com/openshift/nodejs-ex/blob/master/README.md.", 18 | "labels": { 19 | "template": "nodejs-example" 20 | }, 21 | "objects": [ 22 | { 23 | "kind": "Service", 24 | "apiVersion": "v1", 25 | "metadata": { 26 | "name": "${NAME}", 27 | "annotations": { 28 | "description": "Exposes and load balances the application pods" 29 | } 30 | }, 31 | "spec": { 32 | "ports": [ 33 | { 34 | "name": "web", 35 | "port": 8080, 36 | "targetPort": 8080 37 | } 38 | ], 39 | "selector": { 40 | "name": "${NAME}" 41 | } 42 | } 43 | }, 44 | { 45 | "kind": "Route", 46 | "apiVersion": "v1", 47 | "metadata": { 48 | "name": "${NAME}", 49 | "annotations": { 50 | "template.openshift.io/expose-uri": "http://{.spec.host}{.spec.path}" 51 | } 52 | }, 53 | "spec": { 54 | "host": "${APPLICATION_DOMAIN}", 55 | "to": { 56 | "kind": "Service", 57 | "name": "${NAME}" 58 | } 59 | } 60 | }, 61 | { 62 | "kind": "ImageStream", 63 | "apiVersion": "v1", 64 | "metadata": { 65 | "name": "${NAME}", 66 | "annotations": { 67 | "description": "Keeps track of changes in the application image" 68 | } 69 | } 70 | }, 71 | { 72 | "kind": "BuildConfig", 73 | "apiVersion": "v1", 74 | "metadata": { 75 | "name": "${NAME}", 76 | "annotations": { 77 | "description": "Defines how to build the application", 78 | "template.alpha.openshift.io/wait-for-ready": "true" 79 | } 80 | }, 81 | "spec": { 82 | "source": { 83 | "type": "Git", 84 | "git": { 85 | "uri": "${SOURCE_REPOSITORY_URL}", 86 | "ref": "${SOURCE_REPOSITORY_REF}" 87 | }, 88 | "contextDir": "${CONTEXT_DIR}" 89 | }, 90 | "strategy": { 91 | "type": "Source", 92 | "sourceStrategy": { 93 | "from": { 94 | "kind": "ImageStreamTag", 95 | "namespace": "${NAMESPACE}", 96 | "name": "nodejs:6" 97 | }, 98 | "env": [ 99 | { 100 | "name": "NPM_MIRROR", 101 | "value": "${NPM_MIRROR}" 102 | } 103 | ] 104 | } 105 | }, 106 | "output": { 107 | "to": { 108 | "kind": "ImageStreamTag", 109 | "name": "${NAME}:latest" 110 | } 111 | }, 112 | "triggers": [ 113 | { 114 | "type": "ImageChange" 115 | }, 116 | { 117 | "type": "ConfigChange" 118 | }, 119 | { 120 | "type": "GitHub", 121 | "github": { 122 | "secret": "${GITHUB_WEBHOOK_SECRET}" 123 | } 124 | }, 125 | { 126 | "type": "Generic", 127 | "generic": { 128 | "secret": "${GENERIC_WEBHOOK_SECRET}" 129 | } 130 | } 131 | ], 132 | "postCommit": { 133 | "script": "npm test" 134 | } 135 | } 136 | }, 137 | { 138 | "kind": "DeploymentConfig", 139 | "apiVersion": "v1", 140 | "metadata": { 141 | "name": "${NAME}", 142 | "annotations": { 143 | "description": "Defines how to deploy the application server", 144 | "template.alpha.openshift.io/wait-for-ready": "true" 145 | } 146 | }, 147 | "spec": { 148 | "strategy": { 149 | "type": "Rolling" 150 | }, 151 | "triggers": [ 152 | { 153 | "type": "ImageChange", 154 | "imageChangeParams": { 155 | "automatic": true, 156 | "containerNames": [ 157 | "nodejs-example" 158 | ], 159 | "from": { 160 | "kind": "ImageStreamTag", 161 | "name": "${NAME}:latest" 162 | } 163 | } 164 | }, 165 | { 166 | "type": "ConfigChange" 167 | } 168 | ], 169 | "replicas": 1, 170 | "selector": { 171 | "name": "${NAME}" 172 | }, 173 | "template": { 174 | "metadata": { 175 | "name": "${NAME}", 176 | "labels": { 177 | "name": "${NAME}" 178 | } 179 | }, 180 | "spec": { 181 | "containers": [ 182 | { 183 | "name": "nodejs-app", 184 | "image": " ", 185 | "ports": [ 186 | { 187 | "containerPort": 8080 188 | } 189 | ], 190 | "readinessProbe": { 191 | "timeoutSeconds": 3, 192 | "initialDelaySeconds": 3, 193 | "httpGet": { 194 | "path": "/", 195 | "port": 8080 196 | } 197 | }, 198 | "livenessProbe": { 199 | "timeoutSeconds": 3, 200 | "initialDelaySeconds": 30, 201 | "httpGet": { 202 | "path": "/", 203 | "port": 8080 204 | } 205 | }, 206 | "resources": { 207 | "limits": { 208 | "memory": "${MEMORY_LIMIT}" 209 | } 210 | }, 211 | "env": [ 212 | ], 213 | "resources": { 214 | "limits": { 215 | "memory": "${MEMORY_LIMIT}" 216 | } 217 | } 218 | } 219 | ] 220 | } 221 | } 222 | } 223 | } 224 | ], 225 | "parameters": [ 226 | { 227 | "name": "NAME", 228 | "displayName": "Name", 229 | "description": "The name assigned to all of the frontend objects defined in this template.", 230 | "required": true, 231 | "value": "nodejs-example" 232 | }, 233 | { 234 | "name": "NAMESPACE", 235 | "displayName": "Namespace", 236 | "description": "The OpenShift Namespace where the ImageStream resides.", 237 | "required": true, 238 | "value": "openshift" 239 | }, 240 | { 241 | "name": "MEMORY_LIMIT", 242 | "displayName": "Memory Limit", 243 | "description": "Maximum amount of memory the container can use.", 244 | "required": true, 245 | "value": "512Mi" 246 | }, 247 | { 248 | "name": "SOURCE_REPOSITORY_URL", 249 | "displayName": "Git Repository URL", 250 | "description": "The URL of the repository with your application source code.", 251 | "required": true, 252 | "value": "https://github.com/openshift/nodejs-ex.git" 253 | }, 254 | { 255 | "name": "SOURCE_REPOSITORY_REF", 256 | "displayName": "Git Reference", 257 | "description": "Set this to a branch name, tag or other ref of your repository if you are not using the default branch." 258 | }, 259 | { 260 | "name": "CONTEXT_DIR", 261 | "displayName": "Context Directory", 262 | "description": "Set this to the relative path to your project if it is not in the root of your repository." 263 | }, 264 | { 265 | "name": "APPLICATION_DOMAIN", 266 | "displayName": "Application Hostname", 267 | "description": "The exposed hostname that will route to the Node.js service, if left blank a value will be defaulted.", 268 | "value": "" 269 | }, 270 | { 271 | "name": "GITHUB_WEBHOOK_SECRET", 272 | "displayName": "GitHub Webhook Secret", 273 | "description": "Github trigger secret. A difficult to guess string encoded as part of the webhook URL. Not encrypted.", 274 | "generate": "expression", 275 | "from": "[a-zA-Z0-9]{40}" 276 | }, 277 | { 278 | "name": "GENERIC_WEBHOOK_SECRET", 279 | "displayName": "Generic Webhook Secret", 280 | "description": "A secret string used to configure the Generic webhook.", 281 | "generate": "expression", 282 | "from": "[a-zA-Z0-9]{40}" 283 | }, 284 | { 285 | "name": "NPM_MIRROR", 286 | "displayName": "Custom NPM Mirror URL", 287 | "description": "The custom NPM mirror URL", 288 | "value": "" 289 | } 290 | ] 291 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodets-sample", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/src/index.js", 6 | "publishConfig": { 7 | "registry": "http://nexus3-misanche-nexus.apps.na39.openshift.opentlc.com/repository/npm_internal/" 8 | }, 9 | "dependencies": { 10 | "bluebird": "^3.5.1", 11 | "cls-hooked": "^4.2.2", 12 | "cors": "2.8.4", 13 | "debug": "^3.1.0", 14 | "express": "4.16.2", 15 | "js-yaml": "3.10.0", 16 | "node-uuid": "^1.4.8", 17 | "perfy": "^1.1.2", 18 | "prom-client": "11.0.0", 19 | "swagger-tools": "^0.10.4", 20 | "winston": "^2.4.0" 21 | }, 22 | "devDependencies": { 23 | "@types/chai": "4.0.4", 24 | "@types/chai-http": "3.0.3", 25 | "@types/cls-hooked": "^4.2.0", 26 | "@types/debug": "0.0.30", 27 | "@types/express": "4.0.39", 28 | "@types/mocha": "2.2.43", 29 | "@types/node": "8.0.46", 30 | "@types/node-uuid": "0.0.28", 31 | "@types/swagger-tools": "0.10.4", 32 | "@types/winston": "^2.3.7", 33 | "chai": "4.1.2", 34 | "chai-http": "^4.2.0", 35 | "cpx": "1.5.0", 36 | "mocha": "^5.2.0", 37 | "nodemon": "^1.18.6", 38 | "shelljs": "^0.7.8", 39 | "sinon": "^4.1.5", 40 | "supertest": "3.0.0", 41 | "ts-mocha": "1.0.3", 42 | "ts-node": "3.3.0", 43 | "tslint": "5.8.0", 44 | "typescript": "2.5.3" 45 | }, 46 | "scripts": { 47 | "clean": "rm -rf dist", 48 | "build:copy": "cpx \"src/**/*.yaml\" dist/", 49 | "start": "DEBUG=fes:* node dist/index.js", 50 | "build:live": "npm run clean && npm run lint && tsc -p tsconfig.json && npm run build:copy", 51 | "test": "DEBUG=fes:* TS_NODE_CACHE=false ./node_modules/.bin/mocha --compilers ts:ts-node/register ./test/* --exit", 52 | "build:dev": "nodemon --exec ts-node -- ./src/index.ts", 53 | "dev": "DEBUG=fes:* TS_NODE_CACHE=false npm run build:dev", 54 | "lint": "tslint -p tsconfig.json", 55 | "updateSwagger": "node scripts/updateSwagger.js", 56 | "updateAll": "node scripts/updateCode.js", 57 | "testOne": "TS_NODE_CACHE=false ./node_modules/.bin/mocha --compilers ts:ts-node/register " 58 | }, 59 | "engines": { 60 | "node": ">6.11.0" 61 | }, 62 | "author": "Prima", 63 | "license": "ISC" 64 | } 65 | -------------------------------------------------------------------------------- /scripts/updateCode.js: -------------------------------------------------------------------------------- 1 | var pkg=require("../package.json"); 2 | var shelljs=require("shelljs"); 3 | var update=["sos-api"]; 4 | for (var key in pkg.dependencies){ 5 | if (key.indexOf("fes-") === 0 ){ 6 | update.push(key); 7 | } 8 | } 9 | console.log("Tobe updated: ",update); 10 | update.forEach(function(upd){ 11 | shelljs.exec("npm update "+upd); 12 | }) 13 | console.log("Update template"); 14 | shelljs.exec("git fetch upstream"); -------------------------------------------------------------------------------- /scripts/updateSwagger.js: -------------------------------------------------------------------------------- 1 | var pkg=require("../package.json"); 2 | var swagger=pkg.swaggerDefinition; 3 | var path=require("path"); 4 | var fs=require("fs"); 5 | var filePath=path.join(__dirname,"../","node_modules/sos-api/build/swaggers/",swagger+".yaml"); 6 | var outpuPath=path.join(__dirname,"../","src/definition/swagger.yaml"); 7 | if (fs.existsSync(filePath)){ 8 | fs.createReadStream(filePath).pipe(fs.createWriteStream(outpuPath)); 9 | }else{ 10 | console.error("File not exists ",filePath); 11 | } -------------------------------------------------------------------------------- /src/application.ts: -------------------------------------------------------------------------------- 1 | 2 | // import * as bodyParser from "body-parser"; 3 | import * as cors from "cors"; 4 | import * as express from "express"; 5 | import { resolve } from "path"; 6 | import { initSwaggerMiddlware } from "./middlewares/swagger"; 7 | import * as env from "./env"; 8 | import { inOutLogger } from "./log"; 9 | import * as monit from "./monitoring"; 10 | import * as cls from "./lib/cls"; 11 | import { getCorsOptions } from "./cors"; 12 | 13 | env.checkEnv(); 14 | const app = express(); 15 | export default app; 16 | monit.init(app); 17 | app.use(cors(getCorsOptions())); 18 | app.use(cls.setRequestId); 19 | app.use(inOutLogger); 20 | 21 | initSwaggerMiddlware(app, resolve(__dirname), () => { 22 | // self.express.use('/api/weather', helloRouteBuilder); 23 | // self.express.use(bodyParser.json()); 24 | // self.express.use(bodyParser.urlencoded({ extended: false })); 25 | // Custom error handler that returns JSON 26 | app.use(function (err, req: express.Request, res: express.Response, next) { 27 | if (err) { 28 | const errStr = err.message || err.toString(); 29 | const errMsg = { message: errStr, extra: err }; 30 | if (res.statusCode < 400) { 31 | res.status(500); 32 | } 33 | res.json(errMsg); 34 | console.log("test"); 35 | } 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /src/controllers/hello.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import * as P from "bluebird"; 3 | import { TDebug } from "../log"; 4 | 5 | const debug = new TDebug("app:src:controllers:getHelloWorld"); 6 | 7 | export async function getHelloWorld(req: Request, res: Response): P { 8 | const greeting = 9 | req.swagger.params && req.swagger.params.greeting.value ? req.swagger.params.greeting.value : "World"; 10 | debug.log("Greeting: ", greeting); 11 | res.send({"msg": "hello " + greeting}); 12 | } 13 | -------------------------------------------------------------------------------- /src/cors.ts: -------------------------------------------------------------------------------- 1 | import * as env from "./env"; 2 | 3 | export function getCorsOptions(): object { 4 | if (env.get("CORS") !== "*") { 5 | let whiteList: string []; 6 | whiteList = env.get("CORS").split(","); 7 | console.log("CORS whitelist:", whiteList); 8 | return { origin: whiteList }; 9 | } else { 10 | console.log("CORS whitelist: *"); 11 | return { origin: env.get("CORS") }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/definition/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | title: Simple Weather API 4 | description: API for getting the current weather information. 5 | version: '1.0' 6 | produces: 7 | - application/json 8 | host: 'localhost:8001' 9 | basePath: /api 10 | paths: 11 | /hello: 12 | post: 13 | x-swagger-router-controller: helloWorldRoute 14 | operationId: helloWorldPost 15 | tags: 16 | - /hello 17 | description: >- 18 | Returns the current weather for the requested location using the 19 | requested unit. 20 | parameters: 21 | - name: greeting 22 | in: query 23 | description: Name of greeting 24 | required: true 25 | type: string 26 | responses: 27 | '200': 28 | description: Successful request. 29 | schema: 30 | $ref: '#/definitions/Hello' 31 | default: 32 | description: Invalid request. 33 | schema: 34 | $ref: '#/definitions/Error' 35 | get: 36 | x-swagger-router-controller: helloWorldRoute 37 | operationId: helloWorldGet 38 | tags: 39 | - /hello 40 | description: >- 41 | Returns the current weather for the requested location using the 42 | requested unit. 43 | parameters: 44 | - name: greeting 45 | in: query 46 | description: Name of greeting 47 | required: true 48 | type: string 49 | responses: 50 | '200': 51 | description: Successful request. 52 | schema: 53 | $ref: '#/definitions/Hello' 54 | default: 55 | description: Invalid request. 56 | schema: 57 | $ref: '#/definitions/Error' 58 | definitions: 59 | Hello: 60 | properties: 61 | msg: 62 | type: string 63 | required: 64 | - msg 65 | Error: 66 | properties: 67 | message: 68 | type: string 69 | required: 70 | - message 71 | 72 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | 2 | exports.get = get; 3 | exports.set = set; 4 | const def = { 5 | "LOG_LEVEL": "silly", 6 | "CORS" : "*" 7 | }; 8 | 9 | const dynamic = { 10 | 11 | }; 12 | 13 | export function get(key) { 14 | return typeof dynamic[key] !== "undefined" ? 15 | dynamic[key] : 16 | typeof process.env[key] !== "undefined" ? process.env[key] : def[key]; 17 | } 18 | export function set(key, val) { 19 | dynamic[key] = val; 20 | } 21 | 22 | export function checkEnv() { 23 | const log = require("./log").default; 24 | for (const key in def) { 25 | if (process.env[key] === undefined) { 26 | log.warn(`Env var ${key} is not set. Default will be used: ${def[key]}`); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import app from "./application"; 2 | import log from "./log"; 3 | const serverPort = process.env.OPENSHIFT_NODEJS_PORT || 8001; 4 | 5 | app.listen(serverPort, (err) => { 6 | if (err) { 7 | return log.error(err); 8 | } 9 | 10 | return log.info(`server is listening on ${serverPort}`); 11 | }); 12 | -------------------------------------------------------------------------------- /src/lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhappdev/nodejs-template/6bdcd1fb52c22857848126195f09e7318dc9396f/src/lib/.gitkeep -------------------------------------------------------------------------------- /src/lib/asyncHandler.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import * as P from "bluebird"; 3 | import log from "../log"; 4 | import { TDebug } from "../log"; 5 | const debug = new TDebug("src:lib:asyncHandler"); 6 | export interface HandlerOption { 7 | cache?: boolean; 8 | cacheLive?: number; 9 | } 10 | export function asyncHandler( 11 | handler: (req: express.Request, res: express.Response, next) => P, 12 | name: string, options?: HandlerOption): express.Handler { 13 | debug.log("Register handler with option: %o", options); 14 | return (req: express.Request, res: express.Response, next) => { 15 | async function exec(): P { 16 | debug.start("SERVICE:" + name); 17 | if (options && options.cache) { 18 | try { 19 | const item = await getCache(req); 20 | if (item) { 21 | return item.data; 22 | } 23 | } catch (e) { 24 | log.error(e); 25 | console.error(e.stack); 26 | } 27 | 28 | } 29 | const data = await handler(req, res, next); 30 | if (data && options && options.cache) { 31 | cache(req, { 32 | data, 33 | expTs: Date.now() + options.cacheLive ? options.cacheLive : 3600000 34 | }); 35 | } 36 | return data; 37 | } 38 | exec() 39 | .then((data) => { 40 | debug.end("SERVICE:" + name); 41 | if (data) { 42 | res.json(data); 43 | } else if (!res.finished) { 44 | debug.log("no more response to send, status code: %d", res.statusCode); 45 | res.end(); 46 | } 47 | }, (error) => { 48 | debug.end("SERVICE:" + name); 49 | next(error); 50 | }); 51 | }; 52 | } 53 | interface CachedItem { 54 | data: object; 55 | expTs: number; 56 | } 57 | const cachedData: { [key: string]: CachedItem } = {}; 58 | // TODO use cache server 59 | async function cache(req: express.Request, item: CachedItem): P { 60 | const fullUrl = getUrl(req); 61 | debug.log("Set cache: %s data: %o", fullUrl, item); 62 | cachedData[fullUrl] = item; 63 | } 64 | // async function hasCache(req: express.Request): P { 65 | // const fullUrl = getUrl(req); 66 | // return typeof cachedData[fullUrl] !== "undefined"; 67 | // } 68 | async function getCache(req: express.Request): P { 69 | const fullUrl = getUrl(req); 70 | debug.log("Get cache: %s", fullUrl); 71 | const data = cachedData[fullUrl]; 72 | if (data && data.expTs < Date.now()) { 73 | debug.log("Cache expired: %s", fullUrl); 74 | delete cachedData[fullUrl]; 75 | return; 76 | } else { 77 | debug.log("Hit cache: %s", fullUrl); 78 | return data; 79 | } 80 | } 81 | 82 | function getUrl(req: express.Request) { 83 | return req.protocol + "://" + req.get("host") + req.originalUrl; 84 | } 85 | -------------------------------------------------------------------------------- /src/lib/cls.ts: -------------------------------------------------------------------------------- 1 | import * as cls from "cls-hooked"; 2 | import * as uuid from "node-uuid"; 3 | import * as express from "express"; 4 | import * as Debug from "debug"; 5 | import * as P from "bluebird"; 6 | 7 | const debug = Debug("fes:src:lib:namespace"); 8 | 9 | const getNamespace = cls.getNamespace; 10 | const createNamespace = cls.createNamespace; 11 | const NAMESPACE: string = "SOS"; 12 | export const REQ_NAME: string = "X-Request-Id"; 13 | export const request = createNamespace(NAMESPACE); 14 | 15 | export function getRequestId(): string { 16 | const myRequest = getNamespace(NAMESPACE); 17 | if (myRequest && myRequest.get(REQ_NAME)) { 18 | return myRequest.get(REQ_NAME); 19 | } 20 | return undefined; 21 | } 22 | 23 | export async function setRequestId(req: express.Request, res: express.Response, next: express.NextFunction): P { 24 | req[REQ_NAME] = req[REQ_NAME] || req.get(REQ_NAME) || req.query[REQ_NAME] || uuid.v4(); 25 | req.requestId = req[REQ_NAME]; 26 | res.setHeader(REQ_NAME, req[REQ_NAME]); 27 | debug("Response requestId set as: ", res.getHeader(REQ_NAME)); 28 | debug("Store UUID, add it to the request"); 29 | request.run(function() { 30 | request.set(REQ_NAME, req[REQ_NAME]); 31 | debug("CLS, requestId added as:", req[REQ_NAME]); 32 | next(); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | import * as winston from "winston"; 2 | import * as Debug from "debug"; 3 | import * as env from "./env"; 4 | import { getRequestId } from "./lib/cls"; 5 | import { IncomingHttpHeaders, OutgoingHttpHeaders } from "http"; 6 | import * as express from "express"; 7 | import * as P from "bluebird"; 8 | import * as perfy from "perfy"; 9 | 10 | export class TDebug { 11 | 12 | private debugger: debug.IDebugger; 13 | 14 | constructor(namespace: string) { 15 | this.debugger = Debug(namespace); 16 | } 17 | 18 | public log(formatter: string, ...args: any[]) { 19 | this.logger(Levels.Log, formatter, ...args); 20 | } 21 | 22 | public error(formatter: string, ...args: any[]) { 23 | this.logger(Levels.Error, formatter, ...args); 24 | } 25 | 26 | public warn(formatter: string, ...args: any[]) { 27 | this.logger(Levels.Warn, formatter, ...args); 28 | } 29 | 30 | public verbose(formatter: string, ...args: any[]) { 31 | this.logger(Levels.Verbose, formatter, ...args); 32 | } 33 | 34 | public silly(formatter: string, ...args: any[]) { 35 | this.logger(Levels.Silly, formatter, ...args); 36 | } 37 | 38 | public start(label: string) { 39 | perfy.start(getRequestId() + "." + label); 40 | } 41 | 42 | public end(label: string) { 43 | const selector = getRequestId() + "." + label; 44 | if (perfy.exists(selector)) { 45 | const result = perfy.end(getRequestId() + "." + label); 46 | this.logger(Levels.Log, `${label} executed in ${result.time} sec.`); 47 | } 48 | } 49 | 50 | private logger(level: Levels, formatter: string, ...args: any[]) { 51 | const message = getRequestId() ? getRequestId() + " " + level + " " + formatter : formatter; 52 | this.debugger(message, ...args); 53 | } 54 | } 55 | 56 | const debug = new TDebug("fes:src:lib:log"); 57 | 58 | export enum Levels { 59 | Log = "LOG", 60 | Error = "ERROR", 61 | Warn = "WARN", 62 | Verbose = "VERBOSE", 63 | Info = "INFO", 64 | Debug = "DEBUG", 65 | Silly = "SILLY" 66 | } 67 | 68 | export interface RequestLog { 69 | method: string; 70 | originalUrl: string; 71 | requestId: string; 72 | headers: IncomingHttpHeaders; 73 | params: any; 74 | extra?: any; 75 | } 76 | export interface ResponseLog { 77 | statusCode: number; 78 | contentLength: number; 79 | statusMessage: string; 80 | contentType: string; 81 | body?: any; 82 | headers?: OutgoingHttpHeaders; 83 | } 84 | 85 | export async function inOutLogger(req: express.Request, res: express.Response, next: express.NextFunction): P { 86 | const reqLog = { 87 | method : req.method, 88 | originalUrl: req.originalUrl, 89 | requestId: req.requestId, 90 | headers: req.headers, 91 | params: req.query 92 | } as RequestLog; 93 | debug.log("Incoming Request: %O", reqLog); 94 | 95 | const oldWrite = res.write; 96 | const oldEnd = res.end; 97 | 98 | const chunks = []; 99 | 100 | res.write = (...restArgs): boolean => { 101 | if (restArgs[0] && chunks.length === 0) { 102 | chunks.push(new Buffer(restArgs[0])); 103 | } 104 | oldWrite.apply(res, restArgs); 105 | return true; 106 | }; 107 | 108 | res.end = (...restArgs) => { 109 | if (restArgs[0]) { 110 | chunks.push(new Buffer(restArgs[0])); 111 | } 112 | oldEnd.apply(res, restArgs); 113 | logFn(); 114 | }; 115 | 116 | const cleanup = () => { 117 | res.removeListener("close", abortFn); 118 | res.removeListener("error", errorFn); 119 | }; 120 | 121 | const logFn = () => { 122 | cleanup(); 123 | const body = Buffer.concat(chunks).toString("utf8"); 124 | const resLog = { 125 | statusCode: res.statusCode, 126 | statusMessage: res.statusMessage, 127 | contentLength: res.get("Content-Length") || 0, 128 | contentType: res.get("Content-Type"), 129 | body, 130 | headers: res.getHeaders ? res.getHeaders() : undefined // Added in 7.7.0 131 | } as ResponseLog; 132 | if (resLog.statusCode >= 500) { 133 | debug.error("Outgoing Response: %O", resLog); 134 | } else if (resLog.statusCode >= 400) { 135 | debug.warn("Outgoing Response: %O", resLog); 136 | } else { 137 | debug.log("Outgoing Response: %O", resLog); 138 | } 139 | }; 140 | 141 | const abortFn = () => { 142 | cleanup(); 143 | debug.warn("Request aborted by the client"); 144 | }; 145 | 146 | const errorFn = err => { 147 | cleanup(); 148 | debug.error(`Request pipeline error: ${err}`); 149 | }; 150 | 151 | res.on("close", abortFn); // aborted pipeline 152 | res.on("error", errorFn); // pipeline internal error 153 | 154 | next(); 155 | } 156 | 157 | const logger = new (winston.Logger)({ 158 | transports: [ 159 | new (winston.transports.Console)({ 160 | timestamp: true, 161 | "level": env.get("LOG_LEVEL") 162 | }) 163 | ] 164 | }); 165 | 166 | export default logger; 167 | -------------------------------------------------------------------------------- /src/middlewares/swagger.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import swaggerTools = require("swagger-tools"); 3 | import { Swagger20Request } from "swagger-tools"; 4 | import { readFileSync } from "fs"; 5 | import * as YAML from "js-yaml"; 6 | declare module "express" { 7 | interface Request { 8 | swagger: Swagger20Request["swagger"]; 9 | requestId: string; 10 | } 11 | } 12 | const isProd = (process.env.NODE_ENV === "production"); 13 | function loadDocumentSync(file: string): any { 14 | return YAML.load(readFileSync(file)); 15 | } 16 | export const initSwaggerMiddlware = function (app: express.Express, basePath: string, cb: any) { 17 | const swaggerDoc = loadDocumentSync(basePath + "/definition/swagger.yaml"); 18 | const options = { 19 | controllers: basePath + "/routes", 20 | ignoreMissingHandlers: true, 21 | useStubs: false, 22 | validateResponse: true 23 | }; 24 | swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) { 25 | // Interpret Swagger resources and attach metadata to request - must be first in swagger-tools middleware chain 26 | app.use(middleware.swaggerMetadata()); 27 | 28 | // Validate Swagger requests 29 | app.use(middleware.swaggerValidator({ 30 | 31 | })); 32 | 33 | // Route validated requests to appropriate controller 34 | app.use(middleware.swaggerRouter(options)); 35 | if (!isProd) { 36 | // Serve the Swagger documents and Swagger UI 37 | app.use(middleware.swaggerUi()); 38 | } 39 | cb(); 40 | 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/monitoring/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./init"; 2 | export * from "./mw"; 3 | -------------------------------------------------------------------------------- /src/monitoring/init.ts: -------------------------------------------------------------------------------- 1 | import { Express } from "express"; 2 | import promClient = require("prom-client"); 3 | import { requestWatch, reset } from "./mw"; 4 | const pkg = require("../../package.json"); 5 | export function init(app: Express) { 6 | promClient.register.setDefaultLabels({ 7 | fes: pkg.name, 8 | version: pkg.version 9 | }); 10 | promClient.collectDefaultMetrics({ timeout: 30000 }); 11 | app.get("/metrics", (req, res) => { 12 | res.end(promClient.register.metrics()); 13 | reset(); 14 | }); 15 | app.use(requestWatch); 16 | } 17 | -------------------------------------------------------------------------------- /src/monitoring/mw.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import promClient = require("prom-client"); 3 | import "../middlewares/swagger"; 4 | const responseTime = new promClient.Gauge({ 5 | name: "last_response_time", 6 | help: "The time elapse of last http requests", 7 | labelNames: ["method", "path"] 8 | }); 9 | const requestCount = new promClient.Counter({ 10 | name: "request_count", 11 | help: "The request count since application starts", 12 | labelNames: ["method", "path"] 13 | }); 14 | const responseStatCount = new promClient.Counter({ 15 | name: "response_status", 16 | help: "The response status code since application starts", 17 | labelNames: ["method", "path", "statusCode"] 18 | }); 19 | export function requestWatch(req: Request, res: Response, next) { 20 | const labels: any = { 21 | method: req.method, 22 | path: req.path 23 | }; 24 | const timer = responseTime.startTimer(); 25 | requestCount.inc(labels); 26 | res.on("finish", () => { 27 | if (req.swagger) { 28 | labels.path = req.swagger.apiPath; 29 | } 30 | responseStatCount.inc({ 31 | method: labels.method, 32 | path: labels.path, 33 | statusCode: res.statusCode 34 | }); 35 | timer(labels); 36 | }); 37 | next(); 38 | } 39 | export function reset() { 40 | responseTime.reset(); 41 | } 42 | -------------------------------------------------------------------------------- /src/routes/helloWorldRoute.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import {getHelloWorld} from "../controllers/hello"; 3 | import { asyncHandler } from "../lib/asyncHandler"; 4 | export const helloWorldGet = Router().use("/", asyncHandler(getHelloWorld, "helloWorldGet")); 5 | export const helloWorldPost = Router().use("/", asyncHandler(getHelloWorld, "helloWorldPost")); 6 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export const name: string; 4 | export default value; 5 | } -------------------------------------------------------------------------------- /test/application.spec.ts: -------------------------------------------------------------------------------- 1 | import chaiHttp = require("chai-http"); 2 | import * as chai from "chai"; 3 | import app from "../src/application"; 4 | 5 | const expect = chai.expect; 6 | chai.use(chaiHttp); 7 | 8 | describe("App", () => { 9 | it("works", (done: () => void): void => { 10 | chai.request(app) 11 | .get("/api/hello?greeting=world") 12 | .send({}) 13 | .end((err: Error, res: any): void => { 14 | 15 | expect(res.statusCode).to.be.equal(200); 16 | expect(res.body.msg).to.be.equal("hello world"); 17 | done(); 18 | }); 19 | 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/routes/helloWorldRoute.spec.ts: -------------------------------------------------------------------------------- 1 | import chaiHttp = require("chai-http"); 2 | import { initSwaggerMiddlware } from "../../src/middlewares/swagger"; 3 | import app from "../../src/application"; 4 | 5 | import * as chai from "chai"; 6 | const expect = chai.expect; 7 | chai.use(chaiHttp); 8 | describe("Hello World - Test path with parameters ", function () { 9 | it("should be able to return hello xxx", (done: () => void): void => { 10 | 11 | chai.request(app) 12 | .get("/api/hello?greeting=world") 13 | .set("content-type", "application/json") 14 | .send({}) 15 | .end((err: Error, res: any): void => { 16 | 17 | expect(res.statusCode).to.be.equal(200); 18 | expect(res.body.msg).to.be.equal("hello world"); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | 24 | describe("Hello World - Test path with no parameters ", function () { 25 | it("should return an error for missing required parameters", (done: () => void): void => { 26 | 27 | chai.request(app) 28 | .get("/api/hello") 29 | .set("content-type", "application/json") 30 | .send({}) 31 | .end((err: Error, res: any): void => { 32 | 33 | expect(res.statusCode).to.be.equal(400); 34 | expect(res.body.message).to.be.equal("Request validation failed: Parameter (greeting) is required"); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "sourceMap": true 7 | }, 8 | "include": [ 9 | "src/**/*.ts", 10 | "package.json" 11 | ], 12 | "exclude": [ 13 | "test", 14 | "node_modules" 15 | ] 16 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "rules": { 7 | "only-arrow-functions": [ 8 | false 9 | ], 10 | "trailing-comma": [ 11 | true, 12 | { 13 | "multiline": "never", 14 | "singleline": "never" 15 | } 16 | ], 17 | "arrow-parens": false, 18 | "object-literal-sort-keys": false, 19 | "object-literal-key-quotes": [ 20 | false 21 | ], 22 | "max-classes-per-file": [ 23 | false 24 | ], 25 | "no-console": [ 26 | false, 27 | "log" 28 | ], 29 | "variable-name": [ 30 | true, 31 | "ban-keywords", 32 | "check-format", 33 | "allow-pascal-case", 34 | "allow-leading-underscore" 35 | ], 36 | "no-unused-variable": true, 37 | "interface-name": [ 38 | false 39 | ], 40 | "no-empty-interface": false, 41 | "no-string-literal": [ 42 | false 43 | ], 44 | "ordered-imports": false, 45 | "space-before-function-paren": false, 46 | "no-angle-bracket-type-assertion": false, 47 | "no-var-requires": false 48 | } 49 | } --------------------------------------------------------------------------------