A simple Food delivery app in express & typescript using IoC principle (TDD approach) Stage-1.0.0

Hello everyone! I am going to describe a simple food delivery app in express, typescript using the IoC principle. This is the initial stage of the implementation, so from time to time, it will be expended. OK, let’s jump in.

N.B: Prior knowledge of express, typescript, postgres & little bit of docker and docker-compose are required.

Before we jump to code, we should see the ERD of this application though it is in its initial stage. I made this ERD using Lucidchart. You can also try.

N.B: The ERD will be extended from time to time.

What is the IoC principle?
It is a design principle that allows classes to be loosely coupled which is basically necessary for the test & maintenance of an application. There are several implementations like dependency injection, service locator, and factory pattern. Here, we will be going to use Dependency Injection. If you are interested you can check this video. It describes the IoC principle concise & easily understandable way.

We are going to use —
1. Express.
2. TypeScript.
3. Postgres.
4. Docker.
5. InversifyJS. (Lightweight IoC container)
6. TypeORM, Class-transformer, Class-validator.
7. Winston. (A Logger)
8. Jest, Supertest & faker-js.

Our project structure will be —

dockerfiles
env
src
- core
- interfaces
- logs
- middlewares
- migrations
- modules
- shared
- datascource.config.ts
- main.ts
- server.ts
tests
- utils
package.json
package-lock.json
tsconfig.eslint.json
tsconfig.json
.eslintrc
.eslintignore
nodemon.json
docker-compose.dev.yml
jest.config.js

Let’s initialize npm - npm init -y & install the necessary packages —

npm install express@4.17.3 inversify@6.0.1 inversify-express-utils@6.4.3 pg@8.7.3 reflect-metadata@0.1.13 typeorm@0.3.6 winston@3.7.2 class-transformer@0.4.0 class-validator@0.13.2  uuid@8.3.2 --savenpm install @faker-js/faker@6.3.1 @types/express@4.17.13 @types/jest@27.0.2 @types/node@17.0.31 @types/supertest@2.0.12 @typescript-eslint/eslint-plugin@5.20.0 @typescript-eslint/parser@5.20.0 eslint@8.13.0 eslint-config-prettier@8.5.0 eslint-plugin-prettier@4.0.0 jest@27.2.5 nodemon@2.0.15 prettier@2.6.2 supertest@6.2.3 ts-jest@27.0.3 ts-node@10.7.0 tsconfig-paths@4.0.0 typescript@4.6.4 @types/uuid@8.3.4 --save-dev

Create tsconfig.json & tsconfig.eslint.json

// tsconfig.json - you can also create using cmd: tsc --init
{
"compilerOptions": {
/* Language and Environment */
"target": "es2016",
"lib": ["es6"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
/* Modules */
"module": "commonjs",
"rootDir": "./",
"outDir": "./dist",
"types": ["node", "jest"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
/* Type Checking */
"strict": true,
"strictPropertyInitialization": false,
/* Completeness */
"skipLibCheck": true,
},
}
// tsconfig.eslint.json
{
"extends": "./tsconfig.json",
"include": [
"src",
"tests",
".eslintrc"
]
}

Create .eslintrc & .eslintignore for linting (optional)

// .eslintrc
{
"root": true,
"env": {
"node": true,
"commonjs": true
},
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.eslint.json"],
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"ignorePatterns": [".eslintrc.json"],
"rules": {
"quotes": ["error", "single"],
"semi": ["error", "always"],
"indent": ["error", 4],
"no-multi-spaces": ["error"]
}
}
// .eslintignore
node_modules
dist

Create nodemon.json (To run on any changes while saving)

{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/modules/**/*.spec.ts"],
"exec": "npm run start"
}

Let’s add a few scripts in package.json script section —

...
"scripts": {
"lint": "eslint . --ext .js,.ts",
"start": "NODE_ENV=dev ts-node --transpile-only src/main.ts",
"start:dev": "npm run build && node dist/main.js",
"start:watch": "nodemon --trace-warnings",
"build-dev": "tsc && node ./dist/main.js",
"build": "tsc",
"typeorm": "ts-node --require tsconfig-paths/register ./node_modules/typeorm/cli.js -d src/datasource.config.ts",
"typeorm:create": "npm run typeorm migration:create",
"typeorm:generate": "npm run typeorm migration:generate",
"typeorm:run": "npm run typeorm migration:run",
"typeorm:revert": "npm run typeorm migration:revert",
"test": "jest"
}
...
  • typeform, create, generate, run & revert will be used when we do the DB migration.
  • lint for linting the files
  • start- transpile those ts files
  • start:dev- build JS file in dist folder & run with node
  • start:watch- the app will restart on every change.

Let’s add our database. Better to dockerize the application. Isn’t it?
Create env/.env.dev, docker-compose.dev.yml & dockerfiles/Dockerfile-dev files —

Before building the container, let’s create a few important files to make things workable under /src folder —

  • core/container.core.ts
  • core/type.core.ts
  • datasource.config.ts
  • main.ts
  • server.ts

Let’s update one by one —

container.core.ts is important for inversify config. Because it will hold all the files that are going to be added later.

import { Container } from 'inversify';const container = new Container();
export default container;

server.ts & main.ts are responsible for the execution of this application.

// server.ts
import express, { Request, Response, NextFunction } from 'express';
import { InversifyExpressServer } from 'inversify-express-utils';
import container from './core/container.core';
export const server = new InversifyExpressServer(container);
server.setConfig(app => {
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
});
// main.ts
import 'reflect-metadata';
import { server } from './server';
const port: number = Number(process.env.APP_PORT) || 3000;
server.build().listen(port, () => console.log(`Listening on http://localhost:${port}/`));

N.B: reflect-metadata should import only once in your entire application. More about reflect-metadata.

datasource.config.ts — DB config file for Postgres.

import { DataSource } from 'typeorm';const appDataSource = new DataSource({
type: 'postgres',
host: process.env.POSTGRES_HOST,
port: Number(process.env.POSTGRES_PORT),
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: process.env.DATABASE_NAME,
entities: [
__dirname + '/modules/**/entity/*.entity{.ts,.js}',
],
migrations: [
__dirname + '/migrations/**/*{.ts,.js}'
],
migrationsTableName: 'food_app_migrations',
});
export default appDataSource;

N.B: Heading to typeORM doc for more information about datasource.

Let’s build our docker container —

docker-compose -f docker-compose.dev.yml --env-file env/.env.dev build --no-cache

after successfully building the container let’s add more code/files to go further. We just created a data-source file & don’t we need a generic file that will be going to talk with DB? Yes, Indeed. Then create files under the /core folder — /service/database.service.ts & /interface/IDatabase.service.ts.

Add core/type.core.ts file- This file is responsible for all the types that will be used in container.core.ts for injecting.

export const TYPES = {
Logger: Symbol.for('Logger'),
IDatabaseService: Symbol.for('IDatabaseService'),
};

Now update the container.core.ts file —

import { Container } from 'inversify';
import { IDatabaseService } from './interface/IDatabase.service';
import { DatabaseService } from './service/database.service';
import { Logger } from '../shared/services/logger.service';
const container = new Container();container.bind<IDatabaseService>(TYPES.IDatabaseService).to(DatabaseService);
container.bind(TYPES.Logger).to(Logger);
export default container;

You might be thinking why is it necessary to implement an interface (IDatabase.service.ts in this case). Well, uncle bob once said — Top-level class should not depend on the Lower-level class. Why? what if later we change Postgres to any other DB. In that case, we will need to go to all the classes that consume the database service and need to change accordingly which is basically cumbersome. In one sentence, our application should built-in loosely coupled. And as we are following TDD approach development, so it is mandatory for unit tests.

And what are those? (inject() & injectable()). These are from inversify & we are going to inject those classes into other classes also. And one example is the logger.service.ts file. We injected this file into the database service constructor and whenever we will create an instance of the database service class, we don’t need to pass the logger service through the constructor. And this is the expected behavior of the loosely coupled design which we can maintain easily by inversify package. So let’s run the app.

docker-compose -f docker-compose.dev.yml --env-file env/.env.dev up

N.B: I am assuming, you know how the docker works. If you find any error you need to create DB or pgadmin-data/pgdata needs folder permission to use.

SOPPP…. You have done lots of things so far. Let’s create the entity & other related files to complete this tutorial. Don’t worry you will get the Github link in the next chapter. So see you there…

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tushar Roy Chowdhury

Tushar Roy Chowdhury

63 Followers

I am a passionate programmer and always keep eye on new technologies and scrutinize them until getting into shape.