with nodejs and typescript
# BASICS
Node.js is a server-side runtime environment that runs JavaScript code outside of web browsers. It allows developers to build scalable and high-performance network applications using JavaScript.
# BASICS
TypeScript is a syntactic superset of JavaScript which adds static typing.
# Basics
# BASICS
TypeScript allows specifying the types of data being passed around within the code, and has the ability to report errors when the types don't match.
# Basics
# Difference
Clean Architecture is about boundaries and dependencies between "subsystems" and components of your software system. It helps structuring your code for changeability. By that following Clean Architecture will give you a "good" project structure
Clean Code is about the implementation of your "sub systems", components, classes and functions. It helps in structuring your code for readability and maintainability.
# Diagram
If you see in this diagram we have concentric circles which represent layers of any application.
interface IEngine {
start: () => void
stop: () => void
...
}
class PowerfulEngine implements IEngine {
start() {}
stop() {}
// ...
}
class MorePowerfulEngine implements IEngine {
start() {}
stop() {}
// ...
}
class CarBody {
constructor(private engine: IEngine) {}
// ...
}
const car1 = CarBody(new PowerfulEngine())
const car2 = CarBody(new MorePowerfulEngine())
# PRESENTING CODE
# STRUCTURING
Now we will be discussing how node applications can be structured using clean architecture principles, with the help of a Todo application (github repo link).
# LAYERS
Diagram representing our app structure
# LAYER 1
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;
}
# LAYER 2
Controller is responsible for.
# LAYER 2
@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) => {
...
};
}
# LAYER 3
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
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.
# LAYER 4
@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> => { ... };
}
# LAYER 5
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.
# LAYER 5
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>;
}
Common interface
# LAYER 5
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);
}
}
# BENIFITS
By separating the concerns of the various components and enforcing the dependency rule, it becomes much easier to understand and modify the code.
# BENIFITS
Each layer has a specific purpose and is decoupled from the others. This modularity also makes it easier to reuse components in other projects.
# BENIFITS
This can help to catch errors early on in the development process and reduce the overall testing effort.
# BENIFITS
This can be especially useful when it comes to upgrading or replacing technology.