Template for a full-featured MERN stack/Typescript & Swagger/InversifyJS APIs server with nodemon to monitor for any changes in your source and automatically restart Node server
- Content
- I. Features
- II. Using the template
- III. Create the template
- Node/ExpressJS Server with nodemon
- Inversify for DI/IoC to build testable RESTful APIs
- Swagger UI specs & Generates code using swagger-cli tool
- Mongoose & strongly-typed model objects
- Logging
- OAuth2
- Configuration:
- Settings isolation for development/test/uat/production
- CORS: cross-origin config (to allow hosted client in difference server/port)
- Server port config
- Connection string to MongoDB
- Logger
npm install
npm start
chrome http://localhost:3000
npm install
npm run test
# Create package.json
npm init -y
# Install development tools
npm install --save-dev typescript ts-node nodemon @types/debug @types/node @types/express
# Install development libs
npm install --save express
node ./node_modules/typescript/lib/tsc --init # create tsconfig.json
// tsconfig.json
{
"compilerOptions": {
"target": "es6",
"sourceMap": true
}
,"exclude": [
".vscode",
"node_modules",
"bin",
"config"
]
,"include": [
"src/**/*.ts"
]
}
// package.json
..
"scripts": {
"start": "./node_modules/.bin/nodemon --watch src/**/*.ts --exec .\\node_modules\\.bin\\ts-node src/app.ts"
},
..
// src/app.ts
import * as express from 'express';
const app = express();
app.get('/', (req, res) => { res.send(`<b>Time:</b> ${new Date()}`); });
app.listen(3000, () => console.log('Server listening on port 3000!'));
npm start
chrome http://localhost:3000
npm install --save config
npm install --save-dev @types/config
// config/development.json
{
"Port": 3000 // server listen port (default to 3000 if not set)
}
// app/AppConfig.ts
import * as c from "config";
interface IAppConfig extends c.IConfig {
Port: number;
}
var config: IAppConfig = <IAppConfig>c;
export default config;
// src/app.ts
import config from './config/AppConfig';
app.listen(config.Port || 3000, () => console.log(`Server listening on port ${config.Port || 3000}!`));
"scripts": {
"start": "./node_modules/.bin/nodemon --watch src/**/*.ts --watch config/*.json --exec .\\node_modules\\.bin\\ts-node src/app.ts"
},
npm start
# Edit config port to 3001 (in config/development.json)
chrome http://localhost:3000
chrome http://localhost:3001
Please see InversifyJS installation guide for more information
npm install --save inversify reflect-metadata
npm install --save-dev @types/reflect-metadata
moduleResolution set to node (instead of default classic) to solve the issue Cannot find module 'inversify' in VSCode. As there isn't separated typing for @types/inversify accordding to comment from InversifyJS's owner on their npm package.
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "node",
"types": ["reflect-metadata"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Using Mockgoose to mockup the Mongoose connection by an in-memory/temporary MongoDB storage for unit-test code. Mockgoose does not provide typing as it's simple enough to call wrapper function directly.
npm install --save mongoose
npm install --save-dev mockgoose @types/mongoose
// package.json
"scripts": {
"start": "set NODE_ENV=development&& .\\node_modules\\.bin\\nodemon --watch src/**/*.ts --watch config/*.json --exec .\\node_modules\\.bin\\ts-node src/app.ts",
"test": "set NODE_ENV=test&& .\\node_modules\\.bin\\ts-node src/app.ts"
},
// config/development.json
{
"Port": 3000, // server listen port (if n/a then no server wil be created)
"MongoDB": "mongodb://localhost/test"
}
// config/test.json
{
// UNSUED: "Port": 3000,
"MongoDB": "mongodb://example.com/test"
}
// src/config/AppConfig.ts
...
interface IAppConfig extends c.IConfig {
...
MongoDB: string;
...
}
...
// src/app.ts
...
if (config.Port) {
// only create the ExpressJS app if config.Port available
const app = express();
...
}
if (process.env.NODE_ENV === 'test') {
console.log('[UNIT-TEST] is loading..');
...
console.log('[UNIT-TEST] DONE!');
}
// src/lib/MongoClient.ts
import * as mongoose from "mongoose";
export type MongoClient = mongoose.Mongoose;
export const TYPES = {
MongoClient: Symbol("MongoClient"),
};
export async function createMongoClient(connectionString: string) {
return new Promise<MongoClient>((resolve, reject) => {
mongoose.connect(connectionString);
mongoose.connection.on("error", (e: Error) => {
console.log("MongoClient failed to connect to:", connectionString, e);
reject(e);
});
mongoose.connection.once("open", () => {
console.log("MongoClient connected to:", connectionString);
resolve(mongoose);
});
});
}
export async function createMockgoClient(connectionString: string) {
return new Promise<MongoClient>((resolve, reject) => {
var Mockgoose = require('mockgoose').Mockgoose;
var mockgoose = new Mockgoose(mongoose);
mockgoose.prepareStorage().then(function() {
mongoose.connect(connectionString);
mongoose.connection.on("error", (e: Error) => {
console.log("MockgoClient failed to connect to:", connectionString, e);
reject(e);
});
mongoose.connection.once("open", () => {
console.log("MockgoClient connected to:", connectionString);
resolve(mongoose);
});
});
});
}
The bellow code will initialize the Inversify Container with the 1st binding for MongoClient to a pre-initialized instance (global instance), from now on, any instance injections will be returned the same MongoClient instance (hosted by Inversify Container).
// src/app.ts
import "reflect-metadata";
import { Container, inject, injectable } from 'inversify';
import * as mongoclient from './lib/MongoClient';
(async () => {
const container = new Container();
container.bind<mongoclient.MongoClient>(mongoclient.TYPES.MongoClient).toConstantValue(
process.env.NODE_ENV === 'test' ? await mongoclient.createMockgoClient(config.MongoDB) : await mongoclient.createMongoClient(config.MongoDB)
);
})();
Add a injectable Test class with a MongoClient object property, then bind it to the Inversify Container.
// src/app.ts
@injectable()
class Test {
public mongoClient: mongoclient.MongoClient;
public run(): void {
console.log("MongoConnection:", this.mongoClient);
}
}
container.bind<Test>(Test).toSelf();
Next, create the Test instance and run the method to see the value of MongoClient
// src/app.ts
container.get<Test>(Test).run();
Run the code and see console output
npm run test
MongoConnection: undefined
Inject the MongoClient property with Inversify attibute
// src/app.ts
@injectable()
class Test {
@inject(mongoclient.TYPES.MongoClient)
public mongoClient: mongoclient.MongoClient;
...
}
Run the code and see console output again to see the injection is working for now, by automatically assign the injection object property with value taken from Inversify Container
npm run test
MongoConnection: Mongoose {
connections:
...
connect: [Function] }
Clean the testing code and let's move to the next section to apply the DI/IoC to almost every area of the template.