Running the project and testing the business rules

Running the project and testing the business rules

Clean Architecture Series - Frameworks Layer

This is the last article of the clean architecture series with typescript.

In this article we will implement the the Application layer, running the web server and test the business rules making API calls.

App

The app acts as the central point for managing the running process. So, create a file named "app.ts" into the "webserver" folder:

// .src/frameworks/webserver/app.ts
import { Server as ExpressServer } from "@/frameworks/webserver/express/server";
import { SqliteDb } from "../persistence/db/sqlite/sqlite.db";
import { IServer } from "./server.interface";

export class App {
  constructor(private server: IServer) {}
  async bootstrap(): Promise<void> {
    // check the db dependencies
    await SqliteDb.getInstance().connect();
    // framework bootstrapping
    await this.server.bootstrap();
  }

  async run(): Promise<void> {
    await this.server.listen();
    console.log(`Server started at port: ${this.server.port}`);
  }
}

async function start() {
  const port = 9001;
  const app = new App(new ExpressServer(port));
  await app.bootstrap();
  await app.run();
}

start();

bootstrap: The "bootstrap" method checks that dependencies are ready to work (db connection) and initializes the web server.

run: The "run" method run encapsulates the logic to put the server running.

start: The "start" function set the server port, injected the Application Dependencies (web server), initialize the application and run the server.

Running the application

In a terminal, go to the clean architecture folder and run the command npm run start

Testing the business rules

Case 1: Base price is $10 per month

curl  -X POST \
  'http://localhost:9001/policy' \
  --header 'Accept: */*' \
  --header 'User-Agent: Some Agent' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "age": 40,
  "name": "Jhon Doe",
  "sedentary": false,
  "smoker": false,
  "startDate": "2027-12-31"
}'

Response Status: 201 Created:

{
  "payload": {
    "age": 40,
    "name": "Jhon Doe",
    "smoker": false,
    "sedentary": false,
    "startDate": "2026-12-31",
    "price": 10 // <-- base price
  }
}

Case 2: When age is above 50 the price is doubled

We will send the policy member age as 55:

curl  -X POST \
  'http://localhost:9001/policy' \
  --header 'Accept: */*' \
  --header 'User-Agent: Some Agent' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "age": 55,
  "name": "Jhon Doe",
  "sedentary": false,
  "smoker": false,
  "startDate": "2026-12-31"
}'

Response Status: 201 Created

{
  "payload": {
    "age": 55,
    "name": "Jhon Doe",
    "smoker": false,
    "sedentary": false,
    "startDate": "2026-12-31",
    "price": 20 // <-- doubled
  }
}

Case 3: Eligible to apply if age is between 16 to 70 years

We will send the policy member age as 7:

curl  -X POST \
  'http://localhost:9001/policy' \
  --header 'Accept: */*' \
  --header 'User-Agent: Some Agent' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "age": 7,
  "name": "Jhon Doe",
  "sedentary": true,
  "smoker": false,
  "startDate": "2027-12-31"
}'

Response Status: 500 Internal Server Error

{
  "error": {
    "name": "CreatePolicyController",
    "message": "Error",
    "details": "The policy holder age has to be between 16 and 70 years"
  }
}

Case 4: A policy start date must be a date after today

We will send the policy start date with a past date as 2020-12-31:

curl  -X POST \
  'http://localhost:9001/policy' \
  --header 'Accept: */*' \
  --header 'User-Agent: Some Agent' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "age": 20,
  "name": "Jhon Doe",
  "sedentary": false,
  "smoker": false,
  "startDate": "2020-12-31"
}'

Response Status: 500 Internal Server Error

{
  "error": {
    "name": "CreatePolicyController",
    "message": "Error",
    "details": "The policy start date must be after today"
  }
}

Case 5: When the policy holder is smoker the price is multiply by 0.4

We will send the policy member smoker indicator as true:

curl  -X POST \
  'http://localhost:9001/policy' \
  --header 'Accept: */*' \
  --header 'User-Agent: Some Agent' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "age": 40,
  "name": "Jhon Doe",
  "sedentary": false,
  "smoker": true,
  "startDate": "2026-12-31"
}'

Response Status: 201 Created

{
  "payload": {
    "age": 40,
    "name": "Jhon Doe",
    "smoker": true,
    "sedentary": false,
    "startDate": "2026-12-31",
    "price": 14 // <-- base price (10) * smoker (0.4) = 14
  }
}

Case 6: When policy holder has sedentary life style the price is multiply by 0.7

We will send the policy member sedentary indicator as true:

curl  -X POST \
  'http://localhost:9001/policy' \
  --header 'Accept: */*' \
  --header 'User-Agent: Some Agent' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "age": 40,
  "name": "Jhon Doe",
  "sedentary": true,
  "smoker": false,
  "startDate": "2026-12-31"
}'

Response Status: 201 Created

{
  "payload": {
    "age": 40,
    "name": "Jhon Doe",
    "smoker": false,
    "sedentary": true,
    "startDate": "2026-12-31",
    "price": 17 // <-- base price (10) * sedentary (0.7) = 17
  }
}

Let's check the database:

Connect to the Sqlite3 database from the terminal (you should pass your db.sqlite file path, in my case the db file is in the root folder)

sqlite3 db.sqlite

SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite>

Then, we will query the data from the policy table using the SELECT statement:

sqlite> SELECT * from policy;

2|Jhon Doe|40|0|0|2027-12-31|10
3|Jhon Doe|55|0|0|2026-12-31|20
4|Jhon Doe|40|1|0|2026-12-31|14
5|Jhon Doe|40|0|1|2026-12-31|17

Final thoughts

In this journey we learnt about Inversion of Control (IoC), Dependency Injection (DI), types, interfaces, singleton clases, entities and how use it to implement a clean architecture project.

From this point, you can easily swap between tools or framework, and start making for own enhancements. For example:

  • Instead of using ExpressJS you can create your own implementation of the IServer interface and use Fastifyjs as a webserver, then just update the App constructor parameter to use the Fastify implementation.

  • Create the findAll method in the PolicyRepository to fetch all rows from the database.

  • Create a new api routes with controller and use-case to return the policy information.

  • Also, swap between database and replace Sqlite for a non-sql database.

You can get the source files from my GitHub repository.

I hope that this series was useful for you, thanks for your support.

Did you find this article valuable?

Support Max Martínez Cartagena by becoming a sponsor. Any amount is appreciated!