I’m going to start new nodejs project using typescript. Before start I prepared boilerplate solution with classic 3-layered architecture.
The all code lives here: https://github.com/paveldz/node-ts-boilerplate
Each layer is something like separate project in solution.
1) API (nodejs express app)
2) Domain layer (contains Services with business logic)
3) Data access layer
Each project is independent from top API project and has its own package.json and dependencies. The root folder of project contain one more package.json file, this file contains some stuff related to build of whole solution and code analyzis.
Project uses IoC pattern. For these purposes DI-container inversify is used. As a result there is no code with nodejs routes registration, there are controllers that are very similar to ASP.NET MVC controllers. Some inversify code registers routes under the hood. See /Sunmait.Boilerplate.API/controllers/SomeController.ts
Registration of dependencies in DI-container happens in /Sunmait.Boilerplate.API/infrastructure/di/ folder. There are installers for each layer(project) that registers dependencies for DI-container. AllInstaller.ts (/Sunmait.Boilerplate.API/infrastructure/di/AllInstaller.ts) uses every installer. AllInstaller.ts is used on app start (/Sunmait.Boilerplate.API/app.ts file)
import { InstallerBase } from './InstallerBase'; import { ISomeService } from './../../../Sunmait.Boilerplate.Domain/Services/index'; import { SomeService } from './../../../Sunmait.Boilerplate.Domain/Services/Impl/index'; export class DomainInstaller extends InstallerBase { public install(): void { this.container.bind<ISomeService>('SomeService').to(SomeService); } }
–
import { InstallerBase, DomainInstaller, DataInstaller } from './index'; import { ISettingsProvider, SettingsProvider } from '../index'; export class AllInstaller extends InstallerBase { public install(): void { const domainInstaller = new DomainInstaller(this.container); const dataInstaller = new DataInstaller(this.container); domainInstaller.install(); dataInstaller.install(); this.container .bind<ISettingsProvider>('SettingsProvider') .toConstantValue(new SettingsProvider()); } }
For storing app configuration that depends on run environment(for example, dev, qa or prod) config package is used. Configuration values lives into /Sunmait.Boilerplate.API/config/ folder. For providing these values there are ISettingsProvider.ts and SettingsProvider.ts that registered in DI-container and can be injected into any controller.
process.env.NODE_CONFIG_DIR = 'Sunmait.Boilerplate.API/config'; // TODO: fix path import { ISettingsProvider } from './ISettingsProvider'; import * as config from 'config'; // https://github.com/lorenwest/node-config export class SettingsProvider implements ISettingsProvider { public getConnectionString = (): string => config.get('database.connectionString'); }
–
export interface ISettingsProvider { getConnectionString(): string; }
Every folder of every project contains index.ts file. This file exports all files from the folder. It’s something similar to C# namespaces. See example in /Sunmait.Boilerplate.Domain/Services/index.ts.
export * from './ISomeService';
At this moment /Sunmait.Boilerplate.Domain/Services folder contains only one service, therefore there is only one export. Each added service will be exported from this index.ts file.
Example of usage:
import { ISomeService, ISecondService } from './../../../Sunmait.Boilerplate.Domain/Services/index';
I would like to get your opinion, advises and critics on this app architecture and project structure. Thanks a lot!