DEV Community

Cover image for Structuring our Node.JS application
Aniket
Aniket

Posted on

Structuring our Node.JS application

In this segment we will be discussing how node applications can be structured using clean architecture principles, with the help of a Todo application (github repo link).

To make it easier let’s walk through every single layer from the module of the application to acknowledge the core concepts.

We will be using MongoDB, Node, TypeScript, express and inversify to create the application.

What is Inversify?

With the help of inversify you can automate your dependancy injection, instead of every time passing dependancy manually you can create a container and with the help of TS decorators you can easily pass dependancy into your objects, here is example of passing dependancy with and without inversify

with inversify πŸ‘‡

interface ITodoService {...}  

@injectable()  
class TodoService implements ITodoService {  
  @inject(Types.Todo\_REPOSITORY)  
  private todoRepository: ITodoRepository;  
  ...  
}  

interface ITodoRepository {...}  
@injectable()  
class TodoRepository implements ITodoRepository {  
  @inject(Types.ToDo\_TABLE)  
  private todoTable: TodoAppDataSource<ITodoModel\>;  
  ...  
}  


export interface ITodoController {...}  

@injectable()  
class TodoController implements ITodoController {  
  @inject(Types.Todo\_SERVICE)  
  private todoService: ITodoService;  
  ...  
}  

// using todo controller  
const todoController = dIContainer.get<ITodoController\>(Types.Todo\_CONTROLLER);
Enter fullscreen mode Exit fullscreen mode

without inversify πŸ‘‡

interface ITodoService {...}  

class TodoService implements ITodoService {  
  constructor(private todoRepository: ITodoRepository) {...}  
  ...  
}  

interface ITodoRepository {...}  

class TodoRepository implements ITodoRepository {  
  constructor(private todoTable: TodoAppDataSource<ITodoModel>) {...}  
  ...  
}  

export interface ITodoController {...}  

class TodoController implements ITodoController {  
  constructor(private todoService: ITodoService) {...}  
  ...  
}  

// using todo controller  
const todoController = new TodoController(new TodoService(new TodoRepository()))
Enter fullscreen mode Exit fullscreen mode

So you can see from both the example how inversify makes our life easy without inversify every time you want to create instance of TodoController you have to manually pass other 2 dependancies which can be a tedious task as your application grows larger, so it is better to use inversify or some other DI automation tool.

Application layers

Diagram representing our app structure

Sample file structure

Our application is divided into 5 layers, here is the detailed explanation of each layer.

Layer 1 (Routes)

Routes is the inner most layer because it is very rare that you will change any route name so it will be the core of application.

export default function TodoRoutes() {  
  const router = express.Router();  
  const todoController = dIContainer.get<ITodoController>(  
    Types.Todo\_CONTROLLER  
  );  

  router  
    .route("/")  
    .post(todoController.createNewTodo)  
    .get(todoController.getAllTodo)  
    .patch(todoController.updateTodo)  
    .delete(todoController.deleteTodo);  

  return router;  
}
Enter fullscreen mode Exit fullscreen mode

Here if you see the only responsibility of this layer is to register routes and redirect incoming request to its respective controller.

Layer 2 (Controller)

Controller is responsible for

  1. Handling incoming request.
  2. Validating request body.
  3. Sending response to client.

In Below example you see our TodoController is dependant on our TodoService and it implements ITodoService interface so that means all classes that implements ITodoService interface is eligible to be injected in TodoController and this can be used for swapping business logic, it just have to implement ITodoService interface and thats it our TodoController doesn’t needs to know anything about TodoService it will simply adapt with the type of object passed in it.

@injectable()
export class TodoController implements ITodoController {  
@inject(Types.Todo_SERVICE)  

private todoService: ITodoService;  

public createNewTodo = async (req: AuthenticatedRequest, res: Response) => {...};



public getAllTodo = async (  
    req: AuthenticatedRequest,  
    res: Response  
): Promise<Response\> => {...};  
public updateTodo = async (req: AuthenticatedRequest, res: Response) => {...};  
  public deleteTodo = async (req: AuthenticatedRequest, res: Response) => {...};
}
Enter fullscreen mode Exit fullscreen mode

Layer 3 (Service)

Service is our 3rd layer which is responsible of handling our business logic, it is dependant on TodoRepository and any class which satisfy the interface ITodoRepository can be injected in TodoService.

Layer 4 (Repository)

At number 4 we have repository which is responsible of any kind of outer world connection whether it calling any external service API or talking to database all such things are handled in this layer.

@injectable()  
class TodoRepository implements ITodoRepository {  
  @inject(Types.ToDo\_TABLE)  
  private todoTable: TodoAppDataSource<ITodoModel\>;  

  private getParameterObj = (  
    content: string,  
    userId: string  
  ): Omit<ITodoModel, "\_id"\> => ({  
    ...  
  });  

  createNewEntry = async (  
    content: string,  
    userId: string  
  ): Promise<ITodoModel\> => { ... };  

  getAllUserTodo = async (userId: string) => { ... };  

  deleteTodo = async (  
    userId: string,   
    todoId: string  
  ): Promise<ITodoModel\> =>{ ... };  

  updateTodoDetails = async (  
    userId: string,  
    todoId: string,  
    todoDetails: Partial<ITodoModel\>  
  ): Promise<ITodoModel\> => { ... };  
}
Enter fullscreen mode Exit fullscreen mode

As you can see from the above code example our todoTable is injected TodoRepository similarly any class satisfying this TodoAppDataSource can be in injected into TodoRepository

Layer 5 (Database)

At number 5 we have kept our data layer, it encapsulates all our DB queries and expose it with the single interface, there is one base class for each type of DB, refer example shown below for mongoDB and postgresql.

Common interface

export interface TodoAppDataSource<T\> {  
  create(data: T): Promise<T\>;  
  findOne(filter: Partial<T\>, project?: Projection): Promise<T\>;  
  findMany(filter: Partial<T\>, project?: Projection): Promise<T\[\]>;  
  findOneAndUpdate(filter: Partial<T\>, updates: Partial<T\>): Promise<T\>;  
}
Enter fullscreen mode Exit fullscreen mode

MongoDB base class

export class MongoDataSource<T> implements TodoAppDataSource<T> {  
  private table: mongoose.Model<T>;  
  constructor(tableName: DB\_TABLES) {  
    this.table = ALL\_TABLES\[tableName\] as mongoose.Model<T>;  
  }  

  public async findOne<T>(  
    selectQuery: Partial<T>,  
    project: Projection = {}  
  ): Promise<T> {  
    return this.table.findOne(selectQuery as FilterQuery<T>, project);  
  }  

  public async create<T>(data: T): Promise<T> {  
    const newRecord = new this.table(data);  
   return newRecord.save() as Promise<T>;  
  }  

  public async findOneAndUpdate<T>(  
    selectQuery: Partial<T>,  
    updates: Partial<T>  
  ): Promise<T> {  
    return this.table.findOneAndUpdate(selectQuery as FilterQuery<T>, updates, {  
      new: true,  
    });  
  }  

  public findMany = async (  
    filter: Partial<T>,  
    project?: Projection  
  ): Promise<Array<T>> => {  
    const result = await this.table.find(filter as FilterQuery<T>, project);  
    return result as unknown as Promise<Array<T>>;  
  };  
}  

export class UserTable extends MongoDataSource<IUserModel\> {  
  constructor() {  
    super(DB\_TABLES.USER);  
  }  
}
Enter fullscreen mode Exit fullscreen mode

Simillar to above mongoDB queries we can create class for any type of database and can inject this layer (layer 5 β€” DB) to other layer (layer 4 β€” Repository) by just taking care that it implements single interface for all DB base classes.

I know this can be overwhelming for first time readers but trust me once your code repository is setup it will be very easy for you to scale your code, I request you to please go through code from here start from main.ts.

Conclusion

Research is formalised curiosity, it is poking and prying with a purpose. There is no hard and fast rule of setting up or efficiently structuring the code repository. Hence exploring, investigating and experimenting is the best way suggested.

The concise way of writing the code and structuring is the sole reason of this article. Any suggestions or corrections on this article are welcome. Will be back with another interesting article. Happy coding till then !❀

Top comments (0)